
Secure code is a check at the sink, written so the next route cannot skip it.
Out-of-bounds writes, injection, and missing authorization are still the top of MITRE’s list. The foundation is not a framework. It is the habit of encoding, parameterizing, and checking ownership on every path.
The usual mistake is a bootcamp module that never writes the fail-path test.
This page is those habits in one place, with links out to the full guides when you need the current library.
CWE-787 Out-of-bounds Write sits at rank 5 on MITRE’s 2025 CWE Top 25, with 12 mappings in CISA KEV. CWE-20 Improper Input Validation is rank 18. CWE-306 Missing Authentication for Critical Function is rank 21.
Web catalogs multiply. XSS, CSRF, IDOR, and injection are real jobs. They all sit on these three. Memory is whether the process still owns its bytes. Input is whether untrusted data stayed data. Auth is whether the caller is the account you think. Open the buffer overflow and integer overflow guides when the copy is the ticket. Open input validation when the door is the ticket. Open session management when the cookie is the ticket.
Why three layers, not a poster
Tools help. They are not the floor. A Bandit run does not size dest. A Content-Type header does not parse displayName. A green header scan does not prove a login.
2025 CWE Top 25 table. Memory-class rows are thick: CWE-787 at 5, CWE-416 Use After Free at 7, CWE-125 Out-of-bounds Read at 8, CWE-120 at 11, CWE-121 at 14, CWE-122 at 16. Input-class rows sit beside them: CWE-79 first, CWE-89 second, CWE-22 sixth, CWE-20 eighteenth. Authz and authn split across CWE-862, CWE-863, CWE-306, and CWE-639. That is still three stories. Bytes. Data versus grammar. Who is calling.
AUTH load sid from __Host-session regenerate after login userId is now known | INPUT parseBody: type, length, enum displayName is a string, max 80 | MEMORY copy_into(dest, dest_cap, src, src_len) or JS strings you never re-decode as a size | SINK bind SQL, encode HTML, never raw format
Managed runtimes hide the C copy. They do not hide a 2 MB bio, a JSON bomb, or a make([]T, n) from a header. The memory layer still exists. It just moved into caps and parsers.
Memory: dest_cap, then the product
A buffer overflow is a copy that does not know the room. CWE-120 is the classic form. CWE-787 is the broader write. The call that lasts is copy_into(dest, dest_cap, src, src_len). Reject when src_len does not fit. Then memcpy. Then terminate if it is a string. Canaries and ASLR are belts. The OpenSSF Compiler Options Hardening Guide, dated 20 August 2026, still lists -fstack-protector-strong and -D_FORTIFY_SOURCE=3. Turn them on. They do not size dest.
#include <string.h>
#include <stddef.h>
/* Named fallback: one helper, one deny. */
int copy_into(char *dest, size_t dest_cap,
const char *src, size_t src_len) {
if (src_len >= dest_cap) return -1;
memcpy(dest, src, src_len);
dest[src_len] = '\0';
return 0;
}
The product that sizes the allocation is the other memory miss. CWE-190 is the wrap. malloc(n * sizeof *p) can hand you a short block while the loop still walks n. ISO/IEC 9899:2024 added stdckdint.h so ckd_mul can refuse that product. GCC 14 shipped the header in April 2024. calloc(n, size) is specified to fail when the product cannot be represented.
#include <stdlib.h>
void *alloc_or_none(size_t n, size_t size) {
if (n == 0 || size == 0) return NULL;
return calloc(n, size); /* NULL means deny. caller must not copy. */
}
Every caller of alloc_or_none checks the pointer. A NULL is a 400 or a clean abort, not an unchecked dereference. POSIX requires it. ckd_mul does not depend on that promise.
In Node and Go the copy is usually the engine’s job. Your job is the cap. Reject a body over the limit you chose. Do not make([]byte, n) from a raw Content-Length you did not clamp. The integer-overflow sibling is the multiply. The buffer sibling is the write.
Input: the door, then the sink
CWE-20 is a missing or wrong check on a value that later changes control flow or data flow. The door sits at the boundary: query, body, header, cookie, file name, webhook. Semantic rules sit next to syntactic ones. A start date after an end date is a business miss. A string that is not an integer is a type miss.
What the door cannot do: it cannot make <em> safe in HTML. A comment may legally contain that text. It cannot replace a bind. An allowlisted integer still goes in as $1. It cannot pin a directory. A file token you minted is safer than a user path.
Zod 4.4.3 is the parser I actually ship on a JSON handler. Parse once. Reject 400 with a generic body. Log the field name, not a mystery string that looks like a secret.
const { z } = require("zod");
const parseBody = z.object({
displayName: z.string().min(1).max(80),
invoiceId: z.string().uuid(),
});
app.post("/invoices", async (req, res) => {
const parsed = parseBody.safeParse(req.body);
if (!parsed.success) {
res.status(400).send("Bad request");
return;
}
const { displayName, invoiceId } = parsed.data;
const { rows } = await pool.query(
"SELECT id FROM invoices WHERE id = $1 AND user_id = $2",
[invoiceId, req.session.userId]
);
if (!rows[0]) {
res.status(404).send("Not found");
return;
}
res.set("Content-Type", "text/html; charset=utf-8");
res.send(`<p>${escapeHtml(displayName)}</p>`);
});
function escapeHtml(s) {
return String(s)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
}
Identifiers stay displayName, invoiceId, and userId. Client required is kindness. curl skips it. An object where you expected a string is a Mongo operator story. Reject the non-string before it becomes a filter. The input-validation sibling is the allowlist order: closed set, type, length.
Auth: a sid you can revoke
CWE-306 is a critical function with no login. CWE-287 is the broader broken authentication bucket OWASP files under A07:2025. The foundation is a random sid the browser holds and a store record you can drop today. express-session 1.19.0, updated 22 January 2026, still names the cookie connect.sid and omits SameSite unless you set it. Prefer __Host-session.
function loadSession() {
return session({
name: "__Host-session",
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
maxAge: 12 * 60 * 60 * 1000,
},
});
}
app.post("/login", async (req, res) => {
const userId = await verifyPassword(req.body);
if (!userId) {
res.status(401).send("Unauthorized");
return;
}
await new Promise((resolve, reject) => {
req.session.regenerate((err) => (err ? reject(err) : resolve()));
});
req.session.userId = userId;
res.status(204).end();
});
The __Host- prefix refuses Domain. A sibling host cannot plant that name. That is the cookie-toss case. Call regenerate after a password or passkey you trust so the pre-login sid is junk. NIST SP 800-63B-4, nist.gov record dated 1 August 2025, says single-factor passwords SHALL be at least 15 characters. Name the second factor: WebAuthn if the browser ships it, TOTP if you must. SMS is a restricted authenticator in that document.
A JWT waits until another origin must verify without your Redis. jose 6.2.10 published on 21 August 2026. Pin algorithms, aud, iss, and exp. Do not park the blob in localStorage. A JWT in a cookie is still a cookie. You inherited the foreign POST problem. The session sibling is the flags. This page only names the row.
How the three sit under a request
Skip memory and a huge body is a denial before auth runs. Skip input and a valid session still concatenates displayName into SQL. Skip auth and a perfect parser still serves every invoiceId to a stranger. The stack is the point. Headers never replace a layer. helmet 8.3.0, published 12 July 2026, writes CSP and HSTS. Those lines never invoke copy_into, never run parseBody, and never install loadSession.
| Layer | Named check | Fail |
|---|---|---|
| Memory | copy_into, alloc_or_none | reject or abort |
| Input | parseBody, then bind or encode | 400 |
| Auth | loadSession, userId in the query | 401 or 404 |
A10:2025 is Mishandling of Exceptional Conditions. A missing userId is 401, not a scan of every row. A Zod failure is 400, not a schema dump. A NULL from alloc_or_none is deny, not a copy into a pointer you do not have. Fail closed is the shared rule across all three.
Prove each layer this week
You are not overflowing a buffer. You are proving your own helpers return deny.
- Unit-test
copy_intowithsrc_len == dest_cap. Expect-1and an unchanged dest. Unit-testalloc_or_nonewith a count your helper treats as too large. Expect NULL. - POST a body where
displayNameis an object. Expect 400. POST a 2 MB bio if your cap is 80. Expect 400. - DevTools, Application, Cookies. Expect
__Host-session, HttpOnly, Secure, Lax or Strict, Path/, no Domain. - As user A, open user B’s
invoiceIdfrom a fixture. Expect 404, not 200 withcents. - View source on a profile that contains
<em>. Expect escaped text unless that field is the named HTML sink.
/* test_copy.c expect deny when src_len == dest_cap */
char dest[8];
memset(dest, 'A', sizeof dest);
assert(copy_into(dest, sizeof dest, "abcdefgh", 8) == -1);
assert(dest[0] == 'A');
curl -sS -D - -o /dev/null -X POST "https://your-app.example/invoices" \
-H "Cookie: __Host-session=PASTE_FROM_YOUR_DEVTOOLS" \
-H "Content-Type: application/json" \
--data '{"displayName":{"$gt":""},"invoiceId":"00000000-0000-4000-8000-000000000001"}'
# Expect: HTTP/2 400
Questions we keep getting
Does a garbage-collected language skip the memory layer?
No. You still cap lengths. You still refuse a huge allocation from a header. You still keep untrusted data out of a format string or a native addon. The C helpers above are the clear form of the same rule.
Is a JWT a foundation?
No. A JWT is a claims format. The foundation is being able to revoke the caller today. A server-side session record does that. A blob plus a denylist is the same store with extra steps. Use the blob when a peer host must verify without your Redis.
Can I fold all three into "validate input"?
No. A valid UUID still needs userId in the query. A valid string still needs dest_cap or a bind. The door is one layer. It does not replace the other two.



