Secure Coding, Fuzzing & AI-Written Code
The AppSec deep dive describes what breaks. This page is about the engineering habits that stop it breaking: how to write and review code so whole bug classes can't happen, how fuzzing finds the bugs reviewers miss, how to handle code an AI assistant wrote, and how to keep any of it working when a team ships twenty times a day.
Last verified2026-10 Also see: AppSec deep dive (the vulnerability classes) · CI/CD security (where the scanners run) · Dependency management · Agentic AI and MCP
Start here: the basics in plain words
Every security bug in code comes down to the program trusting something it shouldn't. A web form field ends up inside a database query. A file length from the network is used without checking. A user ID in the URL is believed without asking "is this yours?". Secure coding is the set of habits that stop untrusted things from reaching dangerous places.
Two words make code review much easier:
- A source is where untrusted data comes in: an HTTP request, a file upload, a message from a queue, an environment variable an attacker might set, the output of an AI model.
- A sink is where data can do damage: a SQL query, a shell command, an HTML page, a file path, a deserialiser, a redirect.
A vulnerability is a path from a source to a sink with nothing safe in between. The four topics on this page attack that path from different angles:
Write code so the dangerous path can't exist: safe APIs, validation at the boundary, deny-by-default permissions.
Throw millions of automatically generated, malformed inputs at code and watch for crashes. Finds the paths humans didn't imagine.
Code that looks correct and confident but was produced without understanding. Same rules apply, with a few new failure modes.
Many changes a day. Security has to be built into defaults and automation, because nobody can review everything by hand.
Memory hook"follow the data from source to sink". Every finding in a code review is a path where untrusted data reached something dangerous without being made safe.
Secure Coding Practices
What is this?A short list of habits that remove whole classes of bugs, instead of fixing them one at a time. The best ones don't rely on developers remembering anything: they make the safe way the easy way.
Why it mattersMost serious application vulnerabilities (injection, broken access control, unsafe deserialisation, path traversal) are not exotic. They are the same few mistakes, repeated. An engineer who can name the habit that prevents each one, and spot its absence in a diff, is exactly what an AppSec interview is testing.
How it worksThe habits, each with the bug class it removes:
use parameterised queries, argument arrays instead of shell strings, and templating engines that escape output automatically. Removes injection (SQL, command, XSS).
check type, length, range and format once, where data enters, and reject anything unexpected (allow-list, not block-list).
"is this user logged in?" is not enough. Ask "may this user touch this record?" on every read and write. Removes IDOR (insecure direct object reference).
new routes require authentication unless explicitly marked public. An exception in a permission check means "no", never "yes".
no pickle/yaml.load on untrusted data, no hand-rolled crypto, no strcpy. New services in Rust, Go, Java or Python rather than C/C++ remove most memory corruption.
load them at runtime from a secret store. A secret committed to git is in every clone forever, even after you delete the line.
record who did what and when, never passwords, tokens or full card numbers. Show users a generic error, not a stack trace.
In practiceThe same feature written unsafely and safely (Python):
# Unsafe: user input becomes part of the SQL text, and nobody checks ownership
def get_invoice(request, invoice_id):
return db.execute(f"SELECT * FROM invoices WHERE id = {invoice_id}") # ← injection + IDOR
# Safe: input is a bound parameter, and the query itself enforces ownership
def get_invoice(request, invoice_id: int):
row = db.execute(
"SELECT * FROM invoices WHERE id = %s AND owner_id = %s",
(invoice_id, request.user.id), # ← data never becomes code
).fetchone()
if row is None:
raise NotFound() # ← same answer for "missing" and "not yours"
return rowSecurity angleStatic analysis (SAST) tools such as Semgrep and CodeQL automate the source-to-sink search, but they produce false positives and miss logic bugs like missing ownership checks, which have no tell-tale dangerous function. That's why the strongest controls are structural: a framework where every route is authenticated by default, a data-access layer that always filters by tenant, an HTML template engine that always escapes.
📰Real incident — Apple "goto fail" (CVE-2014-1266, 2014). One duplicated line,
goto fail;, in Apple's TLS code made the program jump past the final signature check. Because the error variable still held "success", every connection was accepted, so anyone on the network could impersonate any HTTPS site to iOS and macOS users. Lesson: security-critical code needs tests that prove the "reject" path works, not only that good input is accepted. Compiler warnings for unreachable code and mandatory braces would also have caught it.
🎯On the job. When reviewing a pull request, start from the sinks: grep the diff for query building,
subprocess/exec,innerHTML/dangerouslySetInnerHTML, file paths built from input, deserialisers, redirects and outbound HTTP calls to URLs from input (SSRF). For each one, trace back to its source. For every new route or handler, ask two questions: who may call it, and where is the ownership check?
Software Fuzzing
Last verified2026-10
What is this?Fuzzing is automated testing with garbage. A fuzzer generates huge numbers of slightly broken inputs (truncated files, giant lengths, odd bytes, unexpected field orders), feeds each one to the code, and records any input that makes it crash, hang or break a rule. A human tester might try fifty bad inputs. A fuzzer tries millions an hour.
Why it mattersFuzzers find the bugs nobody thought to test for, especially in code that parses complex input: file formats, network protocols, image and video decoders, compression, certificate parsers. Those are exactly the places attackers aim. By 2023, Google's OSS-Fuzz service, which fuzzes open-source projects continuously, had helped fix over 10,000 vulnerabilities and 36,000 bugs across about 1,000 projects.
How it worksModern fuzzers are coverage-guided: they watch which lines of code each input reaches and keep any input that reaches somewhere new, then mutate that one. The input pool slowly learns the shape of valid data and pushes deeper into the program.
-
Write a harnessdefendonce, ~10 lines
- A small function that takes raw bytes and passes them to the code under test
-
Seed corpusservera few real example inputs
- e.g. 20 valid PNG files, or recorded API requests
-
Mutate and runattackmillions of times
- Flip bits, splice inputs, insert huge numbers; run each one
- Keep inputs that reach new code paths
-
Detect a failureattackcrash, hang, sanitizer report
- Sanitizers (ASan, UBSan) turn silent memory corruption into a loud crash
-
Minimise, fix, keep as a regression testdefendper bug
- The crashing input becomes a permanent unit test
The entry point the fuzzer calls with each generated input. Writing a good one is most of the work.
The collection of interesting inputs the fuzzer keeps and mutates. Save it between runs so fuzzing resumes where it left off.
The fuzzer measures which code each input reached and prefers inputs that found new paths. AFL++ and libFuzzer work this way.
Compiler instrumentation that detects bugs the moment they happen: AddressSanitizer (memory), UndefinedBehaviorSanitizer, MemorySanitizer.
Instead of waiting for a crash, check a rule: "decode(encode(x)) == x", or "two libraries give the same answer".
In practiceA fuzz test in Go, built into the standard toolchain since Go 1.18 (2022):
func FuzzParseHeader(f *testing.F) {
f.Add([]byte("Content-Length: 42\r\n")) // seed corpus
f.Fuzz(func(t *testing.T, data []byte) {
h, err := ParseHeader(data)
if err == nil && h.Length < 0 {
t.Fatalf("negative length accepted: %q", data) // a property, not just "don't crash"
}
})
}$ go test -fuzz=FuzzParseHeader -fuzztime=10m
fuzz: elapsed: 41s, execs: 2914836 (71093/sec), new interesting: 37
--- FAIL: FuzzParseHeader
negative length accepted: "Content-Length: -9223372036854775808\r\n" ← the bug
Failing input written to testdata/fuzz/FuzzParseHeader/7c1e... ← now a permanent regression testThe same idea exists for other languages: libFuzzer and AFL++ for C/C++, cargo-fuzz for Rust, Atheris for Python, Jazzer for Java. For web APIs, schema-based fuzzers (e.g. Schemathesis, RESTler) generate requests from an OpenAPI description.
Security angleFuzzing finds what crashes or breaks a stated rule. It won't find a missing authorization check, because a request that wrongly succeeds doesn't crash anything, unless you write that rule as a property. Coverage also stalls behind checksums, magic numbers and encryption, so good harnesses bypass or fix those up. In CI, run a short fuzz job (minutes) on every pull request and long campaigns (hours or days) on a schedule, and always keep the corpus.
📰Real incident — Heartbleed (CVE-2014-0160, 2014). OpenSSL trusted a length field in a "heartbeat" message and copied that many bytes back from memory, leaking up to 64 KB per request, including private keys and passwords. It was found independently by a Google researcher and by the security company Codenomicon, whose team was testing with its own fuzzing tools. Fuzzing with AddressSanitizer catches this bug class quickly, and Heartbleed is a key reason OSS-Fuzz was created two years later.
📰Real incident — AI-written fuzz target finds an OpenSSL bug (CVE-2024-9143, 2024). Google's OSS-Fuzz team used a language model to write new fuzz harnesses for code that had none. One of them found an out-of-bounds memory write in OpenSSL, in code that had been fuzzed for years. Lesson: the bottleneck in fuzzing is usually harness coverage, not fuzzing time.
🎯On the job. Ask "which of our code parses input from outside, and is any of it fuzzed?" The answer is usually "the file-upload parser, the custom protocol handler and the token decoder, and no". Start with one harness per parser and a 10-minute CI job. When a scanner or an advisory reports a parsing bug in a dependency, check whether the project is in OSS-Fuzz before trusting its quality.
When AI Writes the Code
Last verified2026-10
What is this?Coding assistants (Claude Code, GitHub Copilot, Cursor and others) now write a large share of new code, from single-line completions to agents that make whole pull requests. The code usually compiles and often works. But the model doesn't know your threat model, your tenancy rules or which internal library is the approved one, and it is very good at producing code that looks reviewed.
Why it mattersThe security risks fall into three groups, and interviewers increasingly expect you to name all three:
string-built SQL, missing ownership checks, outdated crypto, disabled TLS verification "to make it work". A 2021 study of Copilot ("Asleep at the Keyboard?") found about 40% of its suggestions in security-relevant scenarios were vulnerable. Models have improved since, but the failure shape hasn't changed: plausible and confident.
models recommend packages that don't exist. A 2025 study of 16 code models found about 20% of recommended packages were hallucinated. Attackers register those names on public registries and wait (slopsquatting).
a coding agent reads repo files, issues and web pages, and can run commands with the developer's credentials. Prompt injection hidden in any of that text can steer it (see Agentic AI and MCP).
How it worksThe practical rule: treat AI output as a contribution from a fast, well-read junior developer who has never seen your codebase and can be tricked by text it reads.
-
Developer prompts the assistantbrowsercontext matters
- Project rules file states the house rules: approved libraries, auth helper, no new dependencies without review
-
Assistant writes or changes codeattackuntrusted until checked
- Watch for: new dependencies, auth/crypto changes, disabled checks, broad permissions
-
Same gates as human codedefendno bypass
- Tests, SAST, secret scanning, dependency checks (does the package exist? how old is it?)
-
A human who understands it approvesdefendownership
- The reviewer must be able to explain the change; "the AI wrote it" is not a review
In practiceWhat a hallucinated dependency looks like in a pull request, and the check that catches it:
+ "dependencies": {
+ "express-session-validator": "^1.0.2" ← plausible name; does it exist? who published it, when?
$ npm view express-session-validator time.created maintainers
time.created = '2026-09-28T03:12:44.000Z' ← created 17 days ago
maintainers = [ 'dev8812 <...>' ] ← one unknown maintainer: block until reviewedSecurity angleControls that work in practice:
CLAUDE.md, .cursorrules, AGENTS.md and similar steer every AI-generated change. Review changes to them like workflow files; hidden instructions in them have been demonstrated as a backdoor technique.
run coding agents in a sandbox or container, without production credentials, with network egress limited and destructive commands requiring approval.
an allow-list or a registry proxy, plus an age check (block packages younger than a few days), stops most slopsquatting.
the same SAST, secret scanning and required review for AI-authored commits; label them so you can measure their defect rate.
pasting a .env file or customer data into an assistant can send it to a third party, depending on the tool's data settings.
📰Real incident — Amazon Q Developer extension (2025). An attacker got a change merged into the open-source repository of Amazon's Q Developer extension for VS Code that added a prompt instructing the AI agent to wipe local files and delete cloud resources. It shipped in version 1.84.0 before AWS pulled it; the instruction was reportedly malformed and didn't execute. Lesson: text aimed at an AI agent is code, and it needs the same review and release controls as code.
📰Real incident — Nx "s1ngularity" npm compromise (2025). Malicious versions of the popular
nxbuild tool were published to npm. Their install script looked for AI coding CLIs on the developer's machine (Claude Code, Gemini CLI, Amazon Q) and ran them with permission checks disabled, asking them to find secrets and crypto wallets, which were then uploaded to public GitHub repositories. Lesson: an AI agent installed on a laptop is a powerful tool any malware can borrow, so its permission prompts should never be bypassed by default.
🎯On the job. In review, ask the author "why this library, and why this approach?". If they can't answer, the code isn't reviewed yet. Add a CI check for newly added dependencies (existence, age, maintainer count, install scripts). Put coding agents' tool permissions, sandbox and allowed commands into your security standards like any other privileged software.
Security in a Fast-Moving Codebase
What is this?Many companies merge hundreds of pull requests a day and deploy continuously, and AI assistants make the volume grow further. A security team of five cannot review all of it. Security for a fast-moving codebase means deciding where human attention goes, and making everything else safe by default and checked automatically.
Why it mattersThe classic model ("security reviews before release") doesn't fit. Either it blocks delivery and gets bypassed, or it rubber-stamps. Interviewers ask "how would you secure a codebase that ships 50 times a day?" to see whether you think in systems (defaults, guardrails, telemetry) or in gatekeeping.
How it worksFour layers, from most to least scalable:
-
1. Paved roads (secure by default)defendprevents bug classes
- One blessed way to do auth, DB access, HTTP clients, secrets, file upload
- Safe defaults in the framework: every route authenticated unless marked public
-
2. Fast, quiet guardrails in CIdefendcatches regressions
- Diff-aware SAST that blocks only high-confidence rules; secret scanning; dependency checks
- Invariant tests, e.g. "every route has an auth decorator"
-
3. Risk-based human reviewserverwhere it matters
- CODEOWNERS sends auth, crypto, payments, IaC and CI workflow changes to security-aware reviewers
- Lightweight threat model for new features that touch data or trust boundaries
-
4. Detect and recover in productionnetworkassume something slips through
- Feature flags and fast rollback; security alerts on auth failures and anomalies
- Bug bounty and fuzzing as continuous outside pressure
In practiceTwo small files that do most of the routing and guarding:
# .github/CODEOWNERS — changes here need a review from the security group
/src/auth/ @acme/appsec
/src/payments/ @acme/appsec
/infra/ @acme/platform-security
/.github/workflows/ @acme/platform-security ← CI is production too# test_routes_require_auth.py — fails the build if anyone adds an unauthenticated route
PUBLIC = {"/health", "/login", "/static"}
def test_every_route_requires_auth():
for rule in app.url_map.iter_rules():
view = app.view_functions[rule.endpoint]
assert rule.rule in PUBLIC or getattr(view, "requires_auth", False), rule.rule # ← invariant, not a review commentSecurity angleThe failure modes are predictable: scanners so noisy that developers learn to click through them, exceptions granted once and never revisited, review fatigue that turns approvals into rubber stamps, and dependency auto-updates that pull in a compromised release within minutes of publication. Counter them by measuring false-positive rates and deleting rules that don't earn their place, giving exceptions an expiry date, keeping security-critical paths small and owned, and adding a short cooldown before auto-merging dependency updates (see the 7-day window).
🎯On the job. When you join a fast-moving team, don't start by reviewing pull requests. Map where the dangerous code lives (auth, tenancy, payments, parsers, CI), put CODEOWNERS on it, find the two or three bug classes that keep recurring in incidents and bug bounty reports, and kill each with a paved road or an invariant test. Then tune CI rules until developers trust a red build.
Memory hook"make the right way the easy way, check it automatically, and save humans for the risky 5%".
Interview Questions
Injection happens when user data becomes part of code, so the fix is structural: parameterised queries through one data-access layer, argument arrays instead of shell strings, and a template engine that escapes output by default. Then I add a SAST rule that flags string-built queries and raw HTML rendering in every pull request, so regressions are caught automatically. Fixing individual findings without changing the default just means the same bug comes back next sprint.
Fuzzing feeds huge numbers of automatically mutated inputs to code and records anything that crashes, hangs or breaks a rule; coverage-guided fuzzers like libFuzzer and AFL++ keep inputs that reach new code, so they work their way deep into parsers. I'd start with whatever parses untrusted input (file uploads, custom protocols, token decoders), build with AddressSanitizer so memory corruption crashes immediately, and run a short job per pull request plus long scheduled campaigns. Heartbleed is the classic example of the bug class fuzzing catches: a trusted length field in a parser.
A fuzzer only notices failures it can observe, like crashes or sanitizer reports, and a request that wrongly succeeds looks perfectly healthy. You can close some of that gap by turning the rule into a property: fuzz API requests as user A against user B's objects and assert the response is always 403 or 404. Differential checks work too, comparing behaviour against a reference. But most authorization bugs are still best prevented by design, like a data layer that always filters by owner.
Three groups: plausible but insecure code, such as string-built SQL or disabled TLS checks (a 2021 Copilot study found about 40% of suggestions in security-relevant scenarios were vulnerable); hallucinated dependencies that attackers register, known as slopsquatting; and the coding agent itself being steered by prompt injection while it holds the developer's credentials. I manage it by treating AI output as untrusted contributions that go through exactly the same tests, SAST and human review, gating new dependencies on existence and age, sandboxing agents without production credentials, and treating rules files like CLAUDE.md as reviewed code.
It's typosquatting's AI-era cousin: code models sometimes recommend packages that don't exist, and they repeat the same made-up names across prompts, so an attacker can register those names on npm or PyPI with malware inside and wait for developers or agents to install them. A 2025 study across 16 models found about one in five recommended packages was hallucinated. The defence is checking every new dependency before install: does it exist, how old is it, who maintains it, does it run install scripts.
I wouldn't try to review everything. I'd build paved roads so the secure way is the default, for example a framework where every route requires auth unless explicitly public, plus fast, low-noise CI checks that block only high-confidence findings, and invariant tests such as "every route has an auth decorator". Human review goes where risk concentrates: CODEOWNERS on auth, payments, infrastructure and CI workflows, and lightweight threat models for features crossing trust boundaries. Then I assume something slips through and invest in detection, fast rollback and a bug bounty, and I measure scanner false-positive rates so developers keep trusting a red build.
No. Passing tests show the code does what the tests check, and AI-written tests often check the happy path the same AI had in mind. The merge requires a human who understands the change and can explain why it's safe: who can call it, what input it trusts, what it adds as dependencies and permissions. That's the same bar as human-written code. The difference is that AI makes it cheap to produce code nobody understands, so the review is where ownership has to be established.