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.
- 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.
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:
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
- Firmware update signing — verifying a signature before flashing new firmware is input validation and least privilege applied to the update mechanism itself.
- Automotive ECU security — UDS's security access seed-key handshake is least privilege enforced at the protocol level.
- IoT credential provisioning — unique per-device certificates (see IoT Architecture) instead of one shared secret across an entire fleet limit the blast radius of any single device's compromise.
- Web API authentication — proper session/token handling and consistent, non-leaking error responses are failing-securely applied to a login flow.
Gotchas
- "We'll add security later" almost never works out — retrofitting it after the architecture is already set is dramatically more expensive and less effective than designing it in from the start.
- Rolling your own cryptography is a near-universal mistake, even for skilled engineers — use well-vetted, widely-reviewed libraries and standard algorithms, never a custom scheme.
- Client-side validation is a UX nicety, never a security boundary — a client can always be bypassed, so the same checks must also happen wherever the trust boundary actually is.
- Logging sensitive data (passwords, tokens, full credential values) is a common, accidental way secrets leak — logs are read by more people and stored for longer than most engineers expect.