Skip to content
Paul Marinos
Menu

Common Insecure Coding Pitfalls

The recurring vulnerability families in real code — injection, deserialization, SSRF into cloud metadata, broken object-level authorization, TOCTOU, and leaked secrets.

The OWASP Top 10 is a useful map and a poor teacher, because a category name doesn’t build the pattern-recognition that catches the bug in a diff. This is the same ground from the implementation side — the specific shapes these flaws take in real code, and why each one recurs. The secure-coding paradigms are the fixes; this is what they’re fixing.

Injection is one bug — untrusted data interpreted as instructions — wearing many interpreters:

  • SQL — the canonical case, fixed by parameterization.
  • Commandos.system(f"convert {filename}") where filename is user-controlled. Pass argument arrays, never a shell string.
  • Template — user input rendered as a template (SSTI), which often reaches code execution because template engines expose object internals.
  • LDAP / NoSQL — the same failure in different query languages. A MongoDB query built from a request body can be steered with {"$gt": ""}.

The recognition skill is spotting untrusted data crossing into an interpreter by string building. The interpreter varies; the tell is the concatenation.

Second-order injection is the variant that defeats naive review: input is stored safely, then later read and used unsafely. The dangerous line contains no user input at all — it uses a database value that originated as user input. This is why “sanitize on input” fails, and why taint analysis tracks data across the whole flow.

Deserializing untrusted data with a format that can instantiate arbitrary objects is remote code execution waiting for a payload. Python’s pickle, Java’s native serialization, and unsafe YAML loaders will construct whatever the bytes describe — including gadget chains that execute on load.

data = pickle.loads(request.body) # attacker controls request.body -> RCE

The fix is categorical: use a data-only format (JSON, or a schema-validated parser) for anything crossing a trust boundary. If you’re deserializing objects from user input, the question is not how to make it safe but why it’s happening at all.

Server-Side Request Forgery — the server makes a request to a URL the user influenced — is a medium-severity bug on-premise and a critical one in cloud, because of a specific target:

requests.get(user_supplied_url) # user points it at 169.254.169.254

That address is the cloud metadata endpoint, and on a misconfigured instance it returns the role’s credentials. SSRF becomes credential theft becomes whatever that role can reach. It is the highest-impact chain in cloud-hosted applications, and it’s why IMDSv2 and egress control are the architectural half of the fix — the code half is an allowlist of destinations, never a blocklist.

The most common serious web vulnerability, and nearly invisible in code because the broken version looks complete:

@app.get("/invoices/<id>")
def invoice(id):
return db.get_invoice(id) # returns ANY invoice, not just the caller's

The code fetches an invoice by id and returns it. What’s missing is the check that this user owns this invoice. Nothing looks wrong — the bug is an absence, which is why scanners miss it and manual authorization testing finds it. The fix is to scope every object access by the authenticated principal, and to enforce it in the data layer rather than the controller where it’s easy to forget on the next endpoint.

Time-of-check to time-of-use: a gap between verifying a condition and acting on it, during which the condition changes.

if account.balance >= amount: # check
# two requests reach here simultaneously, both true
account.balance -= amount # use

Two concurrent requests both pass the check before either debits, and the balance goes negative — the mechanism behind coupon double-spends and withdrawal races. The fix is atomicity: a database constraint, a conditional update (UPDATE ... WHERE balance >= amount), or a lock — never a check followed by a separate action on mutable shared state. These are hard to spot in review precisely because the code reads correctly when executed once.

Endemic and high-impact. A key committed to git is compromised the moment it reaches a repo that anyone else can read, and removing it in a later commit does not help — it’s in the history, and on public repos it’s scraped within minutes.

  • Never commit secrets. Use environment injection or a secret manager.
  • Assume any committed secret is burned. Rotate it — deletion is not remediation.
  • Enforce with pre-commit scanning and push protection, because discipline alone fails eventually.

The detection view is that a leaked-then-used key is often the initial-access event in the timeline, which is why this small mistake carries outsized weight.

Your code is a minority of what ships. Dependency confusion — publishing a malicious public package matching an internal private name, which the resolver then prefers — is one instance of a broader problem: you execute code you didn’t write and rarely read.

Baseline defenses: pin versions and use lockfiles; configure the resolver so internal names can’t be shadowed from public registries; and vet dependencies before adoption, since a package with one maintainer and a recent ownership change is a risk regardless of its download count. The deeper treatment — provenance, signing, SLSA — is CI/CD and platform security.

These are the exploited findings in pentesting, read from the defender’s side. The paradigms prevent them by construction, and scanning is how you find the instances already written. SSRF and secrets reach into cloud; authorization reaches into IAM.

Graph View