Existence checks can leak restricted data
“Does any secret have this value?” is itself a read of secret data. Gate it at least as tightly as reading the value, or it becomes a guessing oracle across permission boundaries.
Read the lesson →An empty list must never mean everyone
Never let an empty list mean “everyone” on a path that sends secrets. When the selection is gone, fail closed and make the user choose again.
Read the lesson →Take audit scope from validated input
Take the scope of an audit decision from the authorisation check, or from validated input, never from raw request fields. Otherwise the person being audited can choose the settings that judge them.
Read the lesson →Start-up scripts must not be world-writable
Anything a container runs later (hooks, entrypoints, cron scripts) must not be writable by the users who run inside it. chmod 777 in a Dockerfile turns “install script” into “anyone's script”.
Quote every value in generated shell scripts
Shell scripts built from template strings need every interpolated value quoted, and quoting has a cost: "~/x" stops meaning your home folder. Decide per variable, and test with a space and a tilde.
Escape secrets before you mask them in logs
Masking secrets is string replacement on untrusted input: escape the values, drop empty ones, mask structured log events before formatting, and check every field, not just the message.
Read the lesson →A POST that exports data is still a read
Don't derive the permission action from the HTTP verb: a POST that exports data is a read. Pass the action explicitly at every route that doesn't follow REST.
User-chosen keys need a null-prototype object
Any object keyed by user input (URLs, names, ids) and loaded from JSON can be prototype-polluted: build it on Object.create(null) or use a Map.
Shared tables need row rules in code
When two features share one table, enforce “who may touch which rows” in code. Then check every read path that skips your service (joins, counts) as well: editable permission presets don't stop new or retyped rows.
Read the lesson →A disabled toggle is not a permission
A disabled switch is not a permission rule: re-check the invariant in the server action. And revoke tokens based on what changed (old grant vs new), not just on what the new grant lacks.
Read the lesson →Lazy regexes on untrusted input
Any parser that runs on inbound email, webhooks or uploads takes attacker-controlled input: a lazy regex over “open tag … close tag” can go quadratic when the close tag never comes.
Read the lesson →Detached DOM nodes still fetch their src
“Detached” is not “inert”: an <img> created in the live document fetches its src immediately, so build untrusted HTML in a separate document (<template> content or DOMParser) before you set any URL.
Pin an approval to the exact version shown
An approval is only meaningful if it names an immutable thing: pin the exact version id into the approved action, and put the approval gate in the tool's execution path, not in the model's instructions.
Read the lesson →Redact streamed secrets across chunk edges
Redacting a stream is not “redact each chunk”: keep back any tail that could be the start of a secret, and never cut output in the middle of a match.
Read the lesson →Give the CI push token to one step only
A token persisted by checkout is readable by every later step, including install scripts from the code under review; give the credential only to the one step that pushes.
Read the lesson →Never accept fields only the server sets
When callers can pre-fill parts of a state machine, check each pre-filled piece against the same invariants the machine counts on (reachability, iteration). Also shape the input so that duplicates and engine-only flags can't be expressed at all.
Read the lesson →Scope what an SSO email assertion unlocks
When an external identity provider vouches for an email, scope what that vouching unlocks. A per-workspace assertion should not flip a global “verified” flag on an account that has its own password.
Read the lesson →Validate templates after variables are filled
Sanitise and derive after template variables are filled in, not before: a URL check that runs on {{url}} will either reject a good link or approve a placeholder. And never guess a data format on a shared write path; convert where the format is known.