Security

Secure Coding Principles

Security isn't a feature bolted on at the end — it's a set of habits applied while writing any code, in any language, for any domain. A security bug is really just a specific kind of correctness bug: a program that trusted input it shouldn't have, or granted more access than a task needed, was always going to fail exactly where someone looking for a way in would check first.

Quick reference
What it is
Language-agnostic habits and design principles that prevent entire classes of vulnerability, rather than reacting to specific bugs after they're found.
Mental model
Every boundary where data or control crosses from something you don't fully trust into something you do is a place a bug becomes a vulnerability.
Key numbers
The OWASP Top 10 (web) and MITRE's CWE Top 25 "Most Dangerous Software Weaknesses" both list largely the same root causes year after year — this isn't a fast-moving target.
Reach for it when
Writing anything that processes untrusted input, handles credentials, or runs with any elevated privilege — in practice, nearly all code.

Principle of least privilege

Give every component, process, and user exactly the access it needs to do its job — no more. A logging routine doesn't need database write access; a sensor-reading task doesn't need network access; a diagnostic service shouldn't unlock reflashing without a separate security access handshake. The point isn't distrust of any one component — it's limiting the blast radius when (not if) something eventually goes wrong: a compromised component with minimal privilege can only do minimal damage.

Input validation: never trust external data

Anything crossing a trust boundary — a network packet, a file, a UART byte stream, a command-line argument, even a sensor reading that hasn't been range-checked — is untrusted until validated.

Untrusted input from the network, files, users, and sensors all pass through a validation gate before reaching trusted internal logic Network Files User input Sensors/peripherals untrusted Validation gate allow-list, bounds, type checks reject anything else Trusted logic only ever sees data that already passed
Everything on the left is a place an attacker gets to choose the bytes — the gate is the one place that choice gets checked before it can influence anything trusted.

Validate against an explicit allow-list of what's acceptable, not a deny-list of known-bad patterns — a deny-list is always incomplete by construction (it can only reject patterns someone already thought of), while an allow-list fails closed by default against everything else.

Memory safety in C

C gives no automatic bounds checking at all, which is exactly why memory-safety bugs (CWE-787, "out-of-bounds write," has topped MITRE's CWE Top 25 for years) dominate real-world vulnerability reports in embedded and systems code specifically:

// Vulnerable: strcpy has no idea how big `dest` is.
// A long enough `src` overwrites memory past the buffer's end --
// including, on the stack, the function's own return address.
char dest[16];
strcpy(dest, src);

// Fixed: the copy can never write past the buffer it's given,
// and the result is guaranteed null-terminated.
char dest[16];
snprintf(dest, sizeof(dest), "%s", src);

The same underlying problem shows up in several shapes: a classic stack buffer overflow (above), an off-by-one loop bound that writes one element past an array, an integer overflow that makes a size-check pass when it shouldn't (allocating len + 1 bytes where len is attacker-controlled and near UINT_MAX wraps to a tiny number), and use-after-free (reading or writing through a pointer after its memory has already been released). All of them come from the same root cause: trusting a size or a lifetime that the code never actually verified.

Secure defaults

A system should be secure out of the box, without anyone having to configure anything extra — default-deny network rules, debug interfaces disabled unless explicitly re-enabled, no blank or hardcoded default password shipped in production firmware. "Secure by default" beats "secure if configured correctly," because most real deployments never revisit the default configuration at all.

Failing securely

When something goes wrong — an exception, malformed input, a resource running out — the failure path itself must not grant more access or leak more information than the success path would. A login system that returns a different error for "wrong password" versus "unknown username" is failing insecurely: it leaks exactly which accounts exist to anyone willing to try logging in repeatedly.

Defense in depth

No single control is assumed sufficient on its own — independent layers each catch what the others miss:

Five independent layers of defense stacked from the network perimeter down to monitoring and logging, each catching what the others miss Network perimeter Input validation Least privilege Encryption Monitoring & logging A gap in any one layer is still caught by the layers around it.
Input validation and parameterized queries and a least-privilege database account together stop SQL injection far more robustly than any one of the three alone.

Secrets management

Never hardcode credentials, API keys, or private keys directly in source code — they end up in version control history permanently, even after being "removed" in a later commit. Use environment variables, a dedicated secrets manager, or — for embedded devices — hardware-backed secure storage tied to the chain of trust established at boot.

Real-world applications

Gotchas

Field notes