Home› Security› Complete Guide
Complete Guide

Complete Security Tutorial

This complete guide covers all 16 Security tutorials on TheCodeForge, organised by topic.

Learning Roadmap
Beginner → Build a strong foundation
Intermediate → Deepen your understanding with practical topics
Advanced → Master advanced concepts and real-world applications
16
Topics
5
Beginner
9
Intermediate
2
Advanced
Jump to section
Injection (1)XSS (2)Auth (5)Crypto (3)Access Control (1)Cloud (1)Supply Chain (2)Secrets (1)

Security bugs are not a separate category of bug. They are ordinary bugs in code that happens to sit on a trust boundary — and the reason they keep recurring is that the boundary is rarely where the author thought it was. SQL injection is a string-building bug. XSS is an escaping bug. SSRF is a bug about which hostnames your server is willing to resolve. Log4Shell was a logging call.

Which makes this track less about attack catalogues and more about one repeated question: at this line, is the data I am about to interpolate, render, deserialise, or fetch coming from somewhere I control? Almost every item below is an answer to that question going wrong, and almost every fix is structural — a parameterised API, a strict allow-list, a verified signature — rather than a filter that tries to recognise bad input.

The one pattern behind injection, XSS and template abuse

Every injection class has the same shape: a string is assembled from code plus data, then handed to an interpreter that cannot tell which part was which. SQL, HTML, shell, LDAP, XPath and log formats are all interpreters. Escaping tries to fix this after the fact by making the data unrecognisable as code, and it works right up until the context changes — the same string is safe in an HTML text node and dangerous inside a <script> block or an onclick attribute.

Parameterisation fixes it at the source by never assembling the string at all. The query and the values travel to the interpreter separately, so there is no point at which the value could be reinterpreted as syntax. That is why it is the only defence that does not degrade as your code grows.

python
# Vulnerable: the value becomes part of the query text
cur.execute(f"SELECT * FROM users WHERE email = '{email}'")

# Safe: placeholders in the SQL, values in a second argument.
# The driver never concatenates; the server receives them apart.
cur.execute("SELECT * FROM users WHERE email = %s", (email,))

# Identifiers cannot be parameterised — allow-list them instead.
SORTABLE = {"created_at", "email", "last_login"}
if sort not in SORTABLE:
    raise ValueError(f"unsortable column: {sort!r}")
cur.execute(f"SELECT * FROM users ORDER BY {sort} DESC LIMIT %s", (limit,))
In practiceThe rule generalises cleanly: pass data as data. subprocess.run([...]) instead of a shell string. A templating engine's auto-escaping instead of manual replacement. Structured logging instead of interpolating user input into a format string — which is precisely the door Log4Shell walked through.

Authentication tokens fail in three specific ways

JWTs concentrate a lot of trust in a small string, and the failures cluster. Signature verification failed is usually a key mismatch after a rotation or a deploy to a different environment — the token is genuine, just signed by a key this service no longer has. An expired token that is still accepted is almost always clock skew: the verifier's leeway is generous, or the two machines disagree about the time.

The third failure is the dangerous one, because it produces no error at all: decoding a token without verifying it. Several libraries expose a decode function that parses the payload and checks nothing, and it is very easy to reach for it while debugging and never take it back out.

SymptomReal causeFix
Signature verification failedKey rotated, wrong environment's secret, or algorithm changed between issuer and verifierPublish a JWKS with key IDs and select the key by the token's kid, so rotation is a non-event
Expired token still acceptedClock skew plus a large leeway window, or expiry simply not being checkedSync clocks with NTP, keep leeway at a few seconds, and assert that exp validation is on
Any token acceptedDecode-without-verify, or accepting the alg the token itself claimsVerify with an explicit algorithm allow-list; never let the token choose how it is checked
Valid token, wrong tenantaud and iss not checkedValidate audience and issuer as strictly as signature

Password storage: the only question is cost

MD5 and SHA-256 are not broken as hashes in the way people usually mean. They are wrong for passwords because they are fast, and speed is exactly what an attacker with a stolen database wants. A modern GPU tries enormous numbers of SHA-256 candidates per second; the same hardware against bcrypt or Argon2 at sane parameters is reduced to a crawl, because those algorithms are deliberately expensive and, in Argon2's case, deliberately memory-hungry so that GPUs lose their advantage.

Salting is a separate concern and solves a different problem: it stops one precomputed table from cracking every account at once. Modern password hashes generate and embed the salt for you, which is why the stored value contains its own parameters — and why you should never implement the format by hand.

python
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

ph = PasswordHasher()            # sane defaults; tune per your hardware

stored = ph.hash(password)       # salt + parameters are inside the string
# $argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>

try:
    ph.verify(stored, submitted)
    if ph.check_needs_rehash(stored):     # parameters raised since signup
        stored = ph.hash(submitted)       # upgrade transparently on login
except VerifyMismatchError:
    pass                                  # generic failure to the caller
In practiceReturn the same generic failure whether the user does not exist or the password is wrong, and spend roughly the same time in both branches. A login endpoint that answers faster for unknown users is an account enumeration API you did not mean to publish.

Server-side trust: SSRF, IDOR and dependency confusion

The three vulnerabilities in this track that hurt mature codebases most are the ones where the code is doing exactly what it was told. An IDOR is an endpoint that loads the record whose id was requested — correctly — without asking whether the caller owns it. SSRF is an image-fetcher or webhook that resolves the hostname it was given, including 169.254.169.254 and anything else inside your network. Dependency confusion is a package manager resolving the highest version it can find, which may be an attacker's upload to a public registry under your internal package name.

None of these are caught by input validation, because the input is well-formed. They are caught by making authority explicit: every query scoped to the caller, every outbound fetch checked against an allow-list after DNS resolution, every internal package name claimed and every registry pinned.

VulnerabilityThe code is doingMake authority explicit by
IDORLoading the requested idScoping every query by owner (WHERE id = ? AND account_id = ?), not by a separate permission check that a new endpoint can forget
SSRFFetching the supplied URLResolving DNS first, rejecting private and link-local ranges, re-checking after redirects, and disabling redirects where you can
Dependency confusionInstalling the newest matching versionScoped names, a single private registry with explicit upstreams, and a committed lockfile
Secrets in git historyStoring what it was committedTreating any pushed secret as burned: rotate first, purge history second, add scanning to pre-commit third

Frequently Asked Questions

If I use an ORM, am I safe from SQL injection?
Mostly, and the exceptions are worth knowing. Any ORM escape hatch that takes raw SQL — raw(), extra(), text(), a hand-built WHERE fragment — is as vulnerable as a plain driver if you interpolate into it. Table and column names also cannot be parameterised in any ORM, so dynamic ORDER BY and dynamic table selection must be allow-listed. The ORM protects the common path; injection now lives in the unusual 10%.
Is CSRF still a real risk with SameSite cookies?
It is reduced, not eliminated. SameSite=Lax is the browser default and blocks the classic cross-site form post, but it still allows top-level navigations, it does nothing for same-site subdomain attacks, and any endpoint that accepts a state change on GET remains exposed. Keep tokens on state-changing endpoints and treat SameSite as defence in depth rather than the whole defence.
What is the difference between 'unable to get local issuer certificate' and an expired certificate?
The first is your side's problem: the server sent a certificate chain your client cannot link back to a trusted root, usually because an intermediate certificate is missing from the server's chain or your trust store is stale. The second is the server's. The distinction matters because the common workaround — disabling verification — turns a fixable configuration issue into a permanent hole.
How should I handle a secret that has already been pushed to GitHub?
Rotate it first. Assume it was scraped within minutes, because public repositories are monitored continuously by people looking for exactly that. Only then rewrite history to purge the blob, force-push, and ask collaborators to re-clone. Purging history without rotating is the common mistake: the commit is gone from your branch but the value is still valid, still cached by forks, and still in the platform's event feed.
Why is Log4Shell described as a logging bug rather than a parsing bug?
Because the vulnerable behaviour was a feature of the logging layer: message strings were scanned for lookup expressions, and the JNDI lookup would fetch and instantiate a remote class. Any code path that logged an attacker-controlled string — a user agent, a username, a search term — was an execution path. It is the clearest example of why interpolating untrusted data into a format string is a trust-boundary decision, not a formatting one.
Where should someone start if security is new to them?
With injection and access control, in that order. Injection because the mechanism generalises to six other vulnerability classes once you see it, and access control because IDOR is the single most common serious finding in real applications and the one least likely to be caught by a scanner. Both are in this track, and both are habits rather than tools.

Injection

XSS

Auth

Crypto

Access Control

Cloud

Supply Chain

Secrets

Also Explore
DevOps 304 tutorials → PHP 56 tutorials → Python 171 tutorials → System Design 145 tutorials → Cloud 12 tutorials → Web Platform 9 tutorials →
Start from the beginning

Every tutorial starts with a plain-English analogy — then real code, then interview questions.

Browse Security Tutorials →