Security Basics Guide

The habits and defences every developer needs β€” threat modeling, password storage, access control, secrets, HTTPS, the OWASP Top 10, input validation, supply-chain safety, secure deployment and incident response. Every code block has a Copy button.

Copy button on each code block  Β·  ● Beginner ● Intermediate ● Advanced

01 Introduction β€” Think Like an Attacker Beginner

Security is not a library you install or a checkbox at the end of a project. It is the set of decisions that keep other people from doing things you did not intend with your system. Most breaches are not exotic β€” they are missed authorization checks, leaked secrets, outdated dependencies and unvalidated input.

The CIA triad

Confidentiality
Only the right people can read the data
Integrity
Data cannot be changed silently
Availability
The service keeps working under attack

Core principles

Threat modeling in five minutes

  1. What are we building? β€” draw the data flow: browser, API, database, third-party services
  2. What can go wrong? β€” walk each trust boundary and ask what an attacker could send, read or replay
  3. What are we going to do? β€” pick a mitigation for each credible threat
  4. Did we do a good job? β€” test it, review it, and repeat when the design changes

STRIDE is a handy checklist for step two: Spoofing identity, Tampering with data, Repudiation of actions, Information disclosure, Denial of service, and Elevation of privilege.

Rule of thumb: If a feature touches money, identity, personal data or admin power, slow down and threat-model it. Everything else still needs the basics β€” but these deserve the extra pass.
⚠ You are a target even if you are small: Automated scanners do not care who you are. Bots find exposed .env files, default admin logins and forgotten staging servers within hours of them going live.
πŸ’‘ Tip: Read the OWASP Top 10 once a year. It is the industry's shared list of the most common, most damaging web application mistakes β€” and this guide covers each category.

02 Passwords, Hashing & Authentication Beginner

Never store passwords in plain text

Store only a slow, salted hash. Hashing is one-way: you verify a login by hashing the attempt and comparing, never by decrypting the stored value.

argon2id
Best default in 2026 β€” memory-hard
bcrypt
Battle-tested, cost factor β‰₯ 12
scrypt
Memory-hard, good alternative
PBKDF2
Acceptable when FIPS is required
MD5 / SHA-1
❌ Never for passwords
SHA-256 alone
❌ Too fast β€” brute-forces easily
// Node.js β€” argon2 (recommended)
import argon2 from "argon2";

const hash = await argon2.hash(password);
// "$argon2id$v=19$m=65536,t=3,p=4$..."

const ok = await argon2.verify(hash, attempt);
// Node.js β€” bcrypt alternative
import bcrypt from "bcrypt";

const hash = await bcrypt.hash(password, 12);
const ok = await bcrypt.compare(attempt, hash);
⚠ Salting is not optional: Modern libraries generate and embed a unique salt automatically. If you are hand-rolling hashes, you are almost certainly doing it wrong. Use a maintained library.

Password policy that actually helps

Sessions vs JWTs

Sessions store state on the server and send an opaque ID in a cookie. JWTs carry signed claims in the token itself and are stateless β€” but stateless means you cannot easily revoke them.

// Session cookie β€” safe defaults
res.cookie("sid", sessionId, {
  httpOnly: true,          // JavaScript cannot read it
  secure: true,            // HTTPS only
  sameSite: "lax",         // blocks most CSRF
  maxAge: 1000 * 60 * 60 * 24,
  path: "/"
});
⚠ Do not store JWTs in localStorage: Any XSS bug can read them. Prefer an httpOnly cookie, keep access tokens short-lived, and use refresh-token rotation with revocation. Never accept alg: "none", and always verify the signature and issuer.
πŸ’‘ Tip: On login, compare with a constant-time function where the library does not already do it, and return the same generic error for β€œunknown user” and β€œwrong password” so attackers cannot enumerate accounts.

03 Authorization & Access Control Intermediate

Authentication answers β€œwho are you?”. Authorization answers β€œwhat are you allowed to do?”. Broken access control is consistently the #1 item on the OWASP Top 10 β€” and it is almost always a missing server-side check.

Models

Deny by default

Every route should require an explicit grant. If a check is missing, the request should fail β€” not succeed.

// Express β€” authorize after authenticating
function requireRole(...roles) {
  return (req, res, next) => {
    if (!req.user) return res.status(401).json({ error: "Unauthorized" });
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: "Forbidden" });
    }
    next();
  };
}

app.delete("/api/users/:id", requireRole("admin"), deleteUser);

IDOR β€” the classic bug

An Insecure Direct Object Reference happens when the app fetches a record by an ID from the request without checking ownership.

// ❌ Any logged-in user can read any invoice
const invoice = await db.invoice.findById(req.params.id);

// βœ… Scope the query to the authenticated user
const invoice = await db.invoice.findOne({
  _id: req.params.id,
  userId: req.user.id
});
if (!invoice) return res.status(404).json({ error: "Not found" });
⚠ Hiding a button is not authorization: The UI is a convenience, not a security boundary. Anyone can call your API directly with curl or DevTools. Check permissions on the server for every request.

Multi-tenant rules

πŸ’‘ Tip: Write authorization as a reusable policy layer (can(user, "delete", post)) instead of scattering if statements. It is easier to test, review and keep consistent.

04 Secrets & Configuration Beginner

A secret is anything that grants access: API keys, database passwords, JWT signing keys, private certificates, OAuth client secrets. Secrets belong in the environment or a secret manager β€” never in source code.

Local development with .env

# .env β€” never commit this file
DATABASE_URL=postgres://app:password@localhost:5432/app
JWT_SECRET=replace-with-a-long-random-value
STRIPE_SECRET_KEY=sk_test_...
NODE_ENV=development
# .gitignore
.env
.env.*
!.env.example
node_modules/
*.pem
*.key
# .env.example β€” commit this as documentation
DATABASE_URL=
JWT_SECRET=
STRIPE_SECRET_KEY=

Reading secrets safely in Node.js

import "dotenv/config";

const secret = process.env.JWT_SECRET;
if (!secret) {
  throw new Error("JWT_SECRET is required");
}

// Fail fast on missing configuration β€” do not boot half-configured
⚠ Anything shipped to the browser is public: A key in frontend JavaScript, a VITE_ variable or a mobile app bundle can be extracted in minutes. Only publish values that are safe for anyone to see.

If a secret leaks

  1. Rotate it immediately β€” revoke the old key and issue a new one. Do this before cleaning history.
  2. Check usage β€” audit logs for unexpected calls or spend.
  3. Remove it from history β€” git filter-repo or the BFG Repo-Cleaner; a normal delete commit still leaves the value in history.
  4. Add prevention β€” pre-commit secret scanning, CI checks, and a .gitignore entry.
# Scan the working tree and full history for common secret patterns
gitleaks detect --source . --log-opts="--all"

# Rewrite history to drop a leaked file (then force-push carefully)
git filter-repo --path .env --invert-paths
πŸ’‘ Tip: In production, prefer a managed secret store (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, HashiCorp Vault) or your platform's encrypted environment variables. Rotate secrets on a schedule and after anyone leaves the team.

05 HTTPS, TLS & Cookies Intermediate

HTTPS encrypts traffic between the browser and your server and proves you are talking to the right server. Without it, passwords, session cookies and API tokens travel in clear text on every coffee-shop Wi-Fi.

What good looks like

# Redirect all HTTP traffic to HTTPS
server {
  listen 80;
  server_name example.com;
  return 301 https://$host$request_uri;
}

# HSTS: one year, include subdomains, allow preload
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

Cookie flags that matter

HttpOnly
Blocks JavaScript access (XSS defence)
Secure
Sent over HTTPS only
SameSite=Lax
Good default for sessions
SameSite=Strict
Strongest CSRF defence
SameSite=None
Requires Secure; for cross-site flows
Max-Age
Keep session lifetimes short
Set-Cookie: sid=abc123; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400
⚠ CORS is not access control: CORS only tells browsers which origins may read responses. It does nothing for curl, scripts or attackers. Never rely on CORS to protect an endpoint β€” authenticate and authorize instead.
πŸ’‘ Tip: Test your TLS setup at ssllabs.com/ssltest and your headers at securityheaders.com. Both give a clear grade and explain every deduction.

06 OWASP Top 10 in Practice Intermediate

The OWASP Top 10 is a consensus list of the most critical web application risks. Learn the categories, then learn the fix for each one.

A01
Broken access control
A02
Cryptographic failures
A03
Injection (SQL, command, XSS)
A04
Insecure design
A05
Security misconfiguration
A06
Vulnerable components
A07
Authentication failures
A08
Data integrity failures
A09
Logging & monitoring failures
A10
Server-side request forgery

XSS β€” cross-site scripting

XSS happens when untrusted data is rendered as HTML or JavaScript. It comes in three flavours: stored (saved in the database), reflected (bounced back in a response) and DOM-based (written by client-side code).

// ❌ Untrusted string interpreted as HTML
element.innerHTML = userComment;

// βœ… Plain text β€” the browser never parses tags
element.textContent = userComment;

// βœ… If HTML is genuinely required, sanitise it first
import DOMPurify from "dompurify";
element.innerHTML = DOMPurify.sanitize(userComment);
⚠ Escape on output, not on input: The same value may be safe in HTML but dangerous in an attribute, URL or script. Encode for the exact context where it is rendered, and avoid innerHTML, eval and document.write with untrusted data.

SQL injection

// ❌ String concatenation β€” attacker controls the query
db.query(`SELECT * FROM users WHERE email = '${email}'`);

// βœ… Parameterised query β€” data stays data
db.query("SELECT * FROM users WHERE email = $1", [email]);

// βœ… Or use an ORM / query builder
await User.findOne({ where: { email } });

CSRF β€” cross-site request forgery

An attacker makes a logged-in user's browser send a state-changing request to your site. Defences: SameSite cookies, a CSRF token checked on every mutating request, and requiring re-authentication for sensitive actions.

// Express β€” CSRF token in a form
app.get("/transfer", (req, res) => {
  res.render("transfer", { csrfToken: req.csrfToken() });
});
app.post("/transfer", csrfProtection, (req, res) => {
  // token was validated by csrfProtection
});

SSRF β€” server-side request forgery

If your server fetches a URL supplied by the user, an attacker can make it reach internal services or cloud metadata endpoints. Allowlist destinations, block private IP ranges, disable redirects and never return raw responses.

// Cloud metadata endpoint β€” a classic SSRF target
http://169.254.169.254/latest/meta-data/iam/security-credentials/

// Defence: allow only expected hosts and schemes
const allowed = new Set(["api.example.com", "images.example.com"]);
πŸ’‘ Tip: Injection is not only SQL. Shell commands, LDAP, NoSQL, template engines and HTTP headers all have their own injection syntax. The universal fix is the same: never build a command by concatenating untrusted strings.

07 Input Validation & Output Encoding Intermediate

Validation decides what data is allowed in. Encoding decides how data is written out. You need both β€” neither is a substitute for the other.

Validate with allowlists

import { z } from "zod";

const Signup = z.object({
  email: z.string().email().max(254),
  password: z.string().min(12).max(128),
  age: z.number().int().min(13).max(120).optional()
}).strict();

const data = Signup.parse(req.body);
// Throws on invalid input; unknown keys are rejected

Encode for the right context

HTML text
Escape < > & " '
Attribute
Quote and HTML-escape values
URL
encodeURIComponent()
JavaScript
JSON.stringify(), never string concat
SQL
Bind parameters
Shell
execFile / spawn with an array
// ❌ Shell injection through exec()
exec(`convert ${filename} out.png`);

// βœ… No shell β€” arguments stay separate
execFile("convert", [filename, "out.png"]);

File uploads

# Path traversal attempt
GET /download?file=../../../../etc/passwd

# Defence: basename + resolve + prefix check
const safe = path.basename(req.query.file);
const full = path.resolve(UPLOAD_DIR, safe);
if (!full.startsWith(path.resolve(UPLOAD_DIR) + path.sep)) {
  return res.status(400).end();
}
πŸ’‘ Tip: Add a Content-Security-Policy as a second line of defence against XSS. Start in report-only mode, fix violations, then enforce. See the deployment section for a starter policy.

08 Dependencies & Supply Chain Advanced

Most modern applications are mostly other people's code. A single compromised or outdated package can hand an attacker your build, your CI secrets or your users' data.

Everyday habits

# Audit the dependency tree
npm audit
npm audit fix

# Install exactly what the lockfile says, ignoring scripts
npm ci --ignore-scripts

# See why a package is installed
npm ls package-name
⚠ The curl … | bash pattern runs whatever the server sends today: Download, read and pin the script instead. The same applies to unpinned GitHub Actions β€” pin third-party actions to a full commit SHA, not a moving tag.

Scripts and browser integrity

<!-- Subresource Integrity: pin the exact file hash -->
<script
  src="https://cdn.example.com/lib.min.js"
  integrity="sha384-BASE64_HASH_HERE"
  crossorigin="anonymous"></script>

Protect the pipeline

πŸ’‘ Tip: Treat a dependency update like a small deploy: it can introduce breaking changes and malicious code. Automated PRs still deserve a quick look at what changed.

09 Secure Deployment & Headers Advanced

Secure code can still be defeated by a misconfigured server. Harden the edges: headers, CORS, rate limits, error handling, logging and backups.

Security headers

Content-Security-Policy
Restrict script/style sources
Strict-Transport-Security
Force HTTPS
X-Content-Type-Options
nosniff β€” stop MIME guessing
Referrer-Policy
Limit leaked URLs
Permissions-Policy
Disable unused browser APIs
frame-ancestors
Blocks clickjacking
# A reasonable starter set (tune the CSP to your app)
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
X-Frame-Options: DENY

Express hardening

import helmet from "helmet";
import rateLimit from "express-rate-limit";

app.use(helmet());
app.disable("x-powered-by");

app.use("/api", rateLimit({
  windowMs: 60_000,
  limit: 100,
  standardHeaders: "draft-7",
  legacyHeaders: false
}));

app.use(express.json({ limit: "100kb" }));

Error handling and logging

Operational basics

⚠ Debug mode in production: Verbose errors, DEBUG=true, source maps and admin panels left open are among the most common real-world exposures. Make β€œproduction” the safe default and require explicit opt-in for diagnostics.
πŸ’‘ Tip: Run a quick external scan after each deploy β€” securityheaders.com, observatory.mozilla.org, and nuclei for known misconfigurations. Fix the easy wins first.

10 Incident Response & Cheat Sheet Advanced

When something goes wrong

  1. Identify β€” confirm the incident, scope what is affected, start a timeline and keep notes
  2. Contain β€” revoke keys, disable the vulnerable endpoint, isolate the host, block the attacker
  3. Eradicate β€” patch the root cause, remove backdoors and malicious packages
  4. Recover β€” restore from a known-good backup, rotate all potentially exposed credentials, monitor closely
  5. Learn β€” write a blameless post-mortem: what happened, what helped, what to change
Rotate before you investigate: If credentials may be exposed, revoke them first. Attackers rarely wait while you read logs, and every extra minute is another chance to use the key.

Developer security checklist

Copy-paste reference

argon2.hash(pw)
Hash a password
argon2.verify(h, pw)
Check a login
HttpOnly; Secure; SameSite
Safe session cookie
$1 parameter
Parameterized SQL
textContent
Safe text insertion
DOMPurify.sanitize()
Sanitise required HTML
npm ci --ignore-scripts
Locked, script-free install
npm audit
Known vulnerabilities
gitleaks detect
Find leaked secrets
helmet()
Sensible security headers
rateLimit()
Stop brute force and abuse
res.status(404)
Do not leak resource existence
Remember: Security is layers, not a single lock. Assume any one control can fail β€” authentication, validation, headers, monitoring β€” and make sure the others still hold.
πŸ’‘ Tip: The highest-return habits are boring: patch dependencies, keep secrets out of Git, check authorization on every request, and never trust input. Do those four consistently and you are ahead of most production codebases.