For developers

Ship secure code without slowing down

Most breaches are not exotic. They are an unchecked permission, a forgotten key in a repository, a dependency nobody updated, a rate limit that was never set. This page is a working guide to application security, secure software delivery and DevSecOps — written for the people who actually write and ship the code.

  • Threat modeling
  • OWASP risks
  • SAST · SCA · DAST
  • Secrets
  • Supply chain
  • API security
Vulnerable login.js
// Input becomes part of the query
const sql = "SELECT * FROM users WHERE email = '"
  + req.query.email + "'";
const rows = await db.query(sql);
Fixed login.js
// Input is bound as data, never parsed as SQL
const rows = await db.query(
  'SELECT id, name FROM users WHERE email = $1',
  [req.query.email]
);
Why it matters

Application security is part of the job, not a gate at the end

Security that arrives after the code is written is expensive, slow and usually negotiable. Security built into how you design, review and ship is cheaper and far harder to skip.

🎯

Applications are the target

Attackers go where the data is, and the data is behind your application logic. Perimeter controls do nothing about a broken authorization check inside a legitimate session.

⏪

Late fixes cost more

A design flaw caught in a whiteboard conversation costs minutes. The same flaw found in production costs an incident, a disclosure and a rewrite of something customers already depend on.

📦

You ship other people's code

Most of a modern application is dependencies, base images, build tooling and CI actions. Your attack surface includes everything you pull in, not just what you wrote.

Secure SDLC

Security activities at every stage of delivery

"Shift left" is not about moving work onto developers. It is about running the cheapest useful check as early as it can run, and keeping the expensive ones for where they pay off.

01 · PLAN

Design

  • Threat model new features and trust boundaries
  • Write abuse cases next to user stories
  • Classify the data you will handle
  • Set security acceptance criteria
02 · CODE

Write

  • Use vetted frameworks and secure defaults
  • Pre-commit secret scanning
  • IDE linting for known-bad patterns
  • Security-aware code review
03 · BUILD

Analyze

  • Static analysis (SAST) on every pull request
  • Dependency scanning (SCA)
  • Generate an SBOM
  • Reproducible, pinned builds
04 · TEST

Attack it

  • Authorization tests for every role
  • Dynamic testing (DAST) on a staging copy
  • Fuzz parsers and API inputs
  • Negative tests for abuse cases
05 · RELEASE

Prove it

  • Sign artifacts and images
  • Record build provenance
  • Require review on protected branches
  • Block unsigned or unscanned releases
06 · DEPLOY

Configure

  • Scan infrastructure-as-code
  • Least-privilege roles and network rules
  • Secrets from a manager, never from the repo
  • Policy-as-code admission checks
07 · OPERATE

Watch

  • Structured, tamper-resistant logging
  • Alert on volume and behavior anomalies
  • Patch SLAs by severity
  • Rehearsed incident response
08 · LEARN

Improve

  • Blameless post-incident reviews
  • Turn each finding into a rule or a test
  • Track time-to-remediate
  • Feed lessons back into design
Threat modeling

Four questions that find design flaws before they exist

Threat modeling is not a document. It is a conversation, held early, with the people who know how the system actually works.

QUESTION 01

What are we building?

Sketch the data flows. Mark where data crosses a trust boundary: browser to API, service to service, your code to a third party.

QUESTION 02

What can go wrong?

Walk each boundary with STRIDE. Ask who could spoof, tamper, deny, leak or escalate here, and what they would gain.

QUESTION 03

What will we do about it?

Mitigate, accept, transfer or eliminate. Write down the decision and the owner so it survives the next reorg.

QUESTION 04

Did we do a good job?

Turn each mitigation into a test. A threat model with no tests is a wish list.

STRIDEViolatesTypical questionTypical control
SpoofingAuthenticationCan someone pretend to be another user or service?Strong auth, MFA, mutual TLS between services
TamperingIntegrityCan data or code be altered in transit or at rest?Signatures, integrity checks, server-side validation
RepudiationNon-repudiationCould a user deny doing something we cannot disprove?Audit logs with identity, time and action
Information disclosureConfidentialityWhere could data leak, including via errors and logs?Encryption, minimal responses, access control
Denial of serviceAvailabilityWhat happens under ten thousand times normal load?Rate limits, quotas, timeouts, back-pressure
Elevation of privilegeAuthorizationCan a low-privilege caller reach a high-privilege action?Deny by default, check on the server, every time
Common risks

The vulnerability classes behind most real incidents

These categories come from the OWASP Top 10, the most widely used awareness list for web application risk. The pattern is more useful than the ranking: the same handful of mistakes, repeated.

Broken access control

What goes wrongThe server trusts the client. Changing an ID in a URL returns someone else's record, or a normal user reaches an admin function.

The fixDeny by default. Check ownership and role on the server for every request, and test authorization separately from authentication.

Injection

What goes wrongUntrusted input is interpreted as code: SQL, shell commands, LDAP, template expressions, NoSQL operators.

The fixParameterized queries and safe APIs. Validate input against an allow-list. Never build commands or queries from strings.

Cryptographic failures

What goes wrongSensitive data sent or stored unprotected, weak or home-made algorithms, hardcoded keys, passwords stored with fast hashes.

The fixTLS everywhere, modern vetted libraries, a slow password hash such as Argon2id or bcrypt, and keys held in a managed service.

Insecure design

What goes wrongThe code is perfect and the idea is flawed: unlimited lookups, a recovery flow that bypasses MFA, an identifier used as proof of identity.

The fixThreat model early, write abuse cases, and use secure design patterns instead of improvising per feature.

Security misconfiguration

What goes wrongDefault credentials, open storage buckets, verbose error pages, unnecessary services, permissive CORS, debug mode left on.

The fixHardened, repeatable baselines, infrastructure-as-code with scanning, and the same configuration review in every environment.

Vulnerable and outdated components

What goes wrongA library with a known flaw sits unpatched because nobody knows it is in the build, or updating it feels risky.

The fixInventory with an SBOM, automated dependency updates, and a patch SLA per severity so the decision is made once, in advance.

Authentication failures

What goes wrongCredential stuffing works, sessions never expire, MFA can be bypassed through recovery, tokens are guessable.

The fixPasskeys or MFA, rate limiting on login, secure session handling, and recovery flows that are as strong as the front door.

Software and data integrity failures

What goes wrongCode or data is trusted without verification: unsigned updates, insecure deserialization, a compromised CI step, a hijacked package.

The fixSign and verify artifacts, lock down the pipeline, never deserialize untrusted data into objects, pin what you consume.

Logging and monitoring failures

What goes wrongAn attacker works for days because nothing records or alerts on abnormal behavior. Breaches get found by accident.

The fixLog security events with identity and context, alert on anomalies in volume and behavior, and rehearse the response.

Server-side request forgery

What goes wrongThe application fetches a URL the attacker supplies and ends up reaching internal services or cloud metadata endpoints.

The fixAllow-list destinations, block internal address ranges, disable unneeded redirects, and enforce egress controls at the network layer.

Category names and their ordering change between editions of the OWASP Top 10. Check the current edition on owasp.org; the underlying mistakes stay remarkably stable.

Best practices

Secure coding practices, by topic

A reference you can keep open during code review. Open a section to see the practices that matter most in it.

Input, output and data handling
  • Treat all input as hostile, including headers, cookies, file names, webhooks and data from your own other services.
  • Validate on the server against an allow-list of expected type, length, format and range. Client-side validation is for usability only.
  • Encode output for its context. HTML, attributes, JavaScript, URLs and SQL each need different encoding. Use a framework that does it by default.
  • Use parameterized queries or a query builder. Never assemble queries, shell commands or template expressions from user strings.
  • Return only what is needed. Explicitly select fields instead of serializing whole database objects, which leaks internal and sensitive attributes.
  • Set a Content Security Policy and other security headers as a second line of defense against cross-site scripting.
Authentication and sessions
  • Prefer passkeys or a proven identity provider over writing your own login. Where passwords remain, hash them with Argon2id or bcrypt with a per-user salt.
  • Require MFA for sensitive accounts and use number-matching or phishing-resistant factors rather than plain approve-or-deny prompts.
  • Rate-limit and lock out smartly on login, password reset and one-time-code endpoints to defeat credential stuffing and brute force.
  • Make recovery as strong as login. A weak reset flow undoes strong authentication.
  • Protect sessions: secure, HttpOnly, SameSite cookies, short lifetimes, rotation on privilege change, and real server-side invalidation on logout.
  • Never use an identifier as an authenticator. Customer numbers, national IDs, emails and dates of birth identify people; they do not prove who is calling.
Authorization
  • Deny by default. New endpoints should be inaccessible until a rule explicitly allows them.
  • Check object-level access on every request. "Is this user logged in" is not the same question as "may this user see this record".
  • Enforce on the server, in one place. Centralize policy in middleware or a policy engine rather than scattering checks through handlers.
  • Return the same response for "does not exist" and "not yours" so attackers cannot enumerate valid identifiers.
  • Test authorization as a matrix: every role against every sensitive endpoint, automated, in CI.
  • Apply least privilege to service accounts and tokens as strictly as to humans.
Secrets and configuration
  • Never commit secrets. Add pre-commit and CI secret scanning, and treat any leaked secret as compromised and rotate it, even after deleting the commit.
  • Use a secrets manager and inject values at runtime. Prefer short-lived, automatically rotated credentials over long-lived static keys.
  • Separate environments completely. Production credentials should not exist on developer laptops or in staging.
  • Keep secrets out of logs, error messages, URLs and container image layers.
  • Scope every key narrowly to one purpose, one service and the minimum permissions it needs.
  • Make rotation routine, not an emergency procedure. If rotating a key is scary, that is a finding.
Cryptography
  • Do not invent cryptography. Use high-level, well-reviewed libraries and their recommended defaults.
  • Encrypt in transit with TLS everywhere, including between internal services, and enforce modern protocol versions.
  • Encrypt sensitive data at rest and manage keys separately from the data they protect.
  • Use cryptographically secure random generators for tokens, IDs and nonces. General-purpose random functions are predictable.
  • Avoid deprecated primitives such as MD5 and SHA-1 for security purposes, and never use ECB mode.
  • Plan for agility: make it possible to change algorithms and rotate keys without a rewrite.
Errors, logging and monitoring
  • Fail closed. When a security check errors, the answer is deny, not allow.
  • Show users generic errors, and log the detail server-side. Stack traces and SQL errors help attackers more than users.
  • Log security events with context: who, what, when, from where, and the outcome. Authentication, access denials, privilege changes and admin actions at minimum.
  • Never log secrets or full personal data. Mask tokens, card numbers and identifiers.
  • Alert on behavior, not only on failure. A sudden jump in lookups from one account is a signal even when every request succeeds.
  • Protect the logs themselves from tampering and ship them off the host so a compromise cannot erase the evidence.
APIs
  • Authenticate and authorize every endpoint, including "internal" ones. Assume the network is hostile.
  • Rate-limit and set quotas per credential, not just per IP. Bulk extraction through a legitimate key is a real attack pattern.
  • Prevent mass assignment. Bind only an explicit allow-list of fields from request bodies, never the whole object.
  • Limit response size and use pagination so a single call cannot return an entire table.
  • Validate against a schema (OpenAPI or JSON Schema) and reject unknown fields and oversized payloads.
  • Keep an inventory of your APIs, including old versions and test endpoints. Forgotten endpoints are the usual way in.
  • Prefer unguessable identifiers and still enforce authorization; obscurity is never the control.
Files, uploads and external content
  • Do not trust file names, extensions or content types. Generate your own storage names and verify content by inspecting it.
  • Store uploads outside the web root or in object storage, served from a separate domain, and never execute them.
  • Limit size and count, and scan for malware where files are shared between users.
  • Guard against path traversal by resolving paths and confirming they stay inside the intended directory.
  • Treat server-side URL fetching as dangerous. Allow-list destinations and block internal ranges to prevent request forgery.
  • Parse untrusted formats defensively and disable risky features such as XML external entities.
Dependencies and third-party code
  • Know what you depend on, directly and transitively. You cannot patch what you cannot see.
  • Commit lockfiles and pin versions so builds are reproducible and a surprise release cannot enter silently.
  • Vet new dependencies. Check maintenance, ownership, download history and whether you really need it. Watch for typosquatted names.
  • Automate updates and run your test suite on them, so patching is a routine merge and not a project.
  • Remove what you do not use. Every package is attack surface you are paying for.
  • Prefer fewer, well-maintained libraries over many small ones that each add maintainer risk.
In code

Three bugs you will see in review, and the fix

Short, language-agnostic ideas shown in JavaScript. The principle carries to any stack.

1. Broken access control: reading someone else's record

The user is authenticated, so the request passes. Nothing checks that the invoice belongs to them. Change the ID, read the next customer's data.

Vulnerable invoices.js
app.get('/api/invoices/:id', auth, async (req, res) => {
  // any logged-in user can read any invoice
  const invoice = await Invoice.findById(req.params.id);
  res.json(invoice);
});
Fixed invoices.js
app.get('/api/invoices/:id', auth, async (req, res) => {
  const invoice = await Invoice.findOne({
    _id: req.params.id,
    ownerId: req.user.id          // enforce ownership
  });
  // same answer for "missing" and "not yours"
  if (!invoice) return res.sendStatus(404);
  res.json(invoice);
});

2. Unbounded lookups: the endpoint that exports your database

Each request is legitimate and each response is correct. The flaw is that nothing limits how many you can make. A lookup service with predictable identifiers and no volume control is a bulk export waiting for a patient attacker, which is exactly the lesson of our CPR register briefing.

Vulnerable lookup.js
app.get('/api/lookup/:personId', apiKeyAuth, async (req, res) => {
  // unlimited calls, no baseline, no alert
  res.json(await People.find(req.params.personId));
});
Fixed lookup.js
// quota per credential, not per IP
const limiter = rateLimit({
  windowMs: 60_000,
  max: 60,
  keyGenerator: (req) => req.apiKey.id
});
app.use('/api/lookup', limiter);

// alert on anomalies even when every call succeeds
if (usage.today > baseline * 10) {
  alerts.notify('lookup volume anomaly', { key: apiKey.id });
}

3. Mass assignment: letting the client choose its own role

Binding the whole request body to a model is convenient, until someone adds "role": "admin" to the JSON.

Vulnerable users.js
app.post('/api/users', async (req, res) => {
  // whatever the client sends is saved
  const user = await User.create(req.body);
  res.status(201).json(user);
});
Fixed users.js
app.post('/api/users', async (req, res) => {
  // allow-list the fields; set privileged ones server-side
  const { name, email } = req.body;
  const user = await User.create({ name, email, role: 'member' });
  res.status(201).json({ id: user.id, name, email });
});
DevSecOps

A security pipeline that developers do not route around

DevSecOps is not a tool category. It is the practice of making secure delivery the path of least resistance, with automated checks that are fast, trusted and actionable.

StageControlWhat it catchesSensible gate
Pre-commitSecret scanning, lintersCredentials and keys about to enter the repositoryBlock the commit
Pull requestSAST, dependency review, required code reviewInjection patterns, unsafe APIs, risky new dependenciesFail on new high-severity findings
BuildSCA, SBOM generation, image scanning, license checksKnown-vulnerable libraries and base imagesFail on critical issues with an available fix
TestDAST on an ephemeral environment, API fuzzing, authorization testsRuntime flaws, broken access control, misconfigured endpointsFail on confirmed high-severity results
ReleaseArtifact signing, provenance attestationTampering between build and deployBlock unsigned or unverified artifacts
DeployIaC scanning, policy-as-code, admission controlPublic storage, privileged containers, over-broad IAMBlock policy violations
RunRuntime monitoring, rate limits, logging, vulnerability managementActive exploitation, drift, abnormal usageAlert, ticket, and meet the patch SLA

Making it work in practice

⚡

Keep it fast

A scan that adds twenty minutes to every build gets disabled. Run quick checks on every commit, heavy ones nightly or on merge, and cache aggressively.

🎚️

Tune before you enforce

Start in report-only mode, fix the noise, then block on a narrow set of high-confidence findings. Widen the gate over time. A pipeline that cries wolf gets ignored.

🧭

Findings need owners

Route issues into the same backlog as bugs, with severity, an owner and an SLA. An unowned dashboard is a graveyard, not a control.

🛤️

Build a paved road

Provide templates, shared pipeline steps and hardened base images that are secure by default. Teams adopt the easy path far more reliably than a policy document.

🧑‍🤝‍🧑

Grow security champions

One interested developer per team, with a little time and training, scales security better than a central team trying to review everything.

📊

Measure what changes behavior

Track time-to-remediate, share of services with scanning enabled, age of dependencies and repeat vulnerability classes. Avoid vanity counts of findings.

Supply chain

Secure what you pull in, how you build it, and what you ship

Compromising one popular package, one build step or one CI token can reach thousands of downstream applications. The defenses are unglamorous and effective.

📋

Know what you ship

Generate a Software Bill of Materials for every build, store it with the artifact, and use it to answer "are we affected" within minutes when the next major library flaw is announced.

🔒

Lock down what you consume

Commit lockfiles, pin versions, pull from a controlled registry or proxy, review every new dependency, and watch for lookalike package names. Treat a dependency update as a code change.

🏗️

Protect the build pipeline

Give CI jobs the minimum permissions, prefer short-lived federated credentials over stored tokens, pin third-party actions to a full commit hash, use ephemeral runners, and require review on protected branches.

✍️

Prove what you built

Sign artifacts and container images, record build provenance, and verify signatures at deploy time so that only what your pipeline produced can run. The SLSA framework gives a maturity ladder to follow.

Containers & cloud

Secure the platform your code runs on

🐳

Containers

Use minimal base images, run as a non-root user, make the filesystem read-only where you can, scan images in CI and in the registry, and keep secrets out of image layers.

🧱

Infrastructure as code

Define infrastructure in version control, scan templates before apply, deny public storage by default and enforce policies as code so a misconfiguration fails a build, not an audit.

☁️

Cloud identity & network

Least-privilege roles per workload, no long-lived admin keys, network segmentation and egress control, separate accounts for production, and audit logging switched on from day one.

Checklist

Where does your team stand?

An honest self-assessment. Tick what is genuinely true for your main service. Your progress is saved in this browser only.

0 of 0 complete

Design & code

Pipeline

Runtime

People & process