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.
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.
// Input becomes part of the query
const sql = "SELECT * FROM users WHERE email = '"
+ req.query.email + "'";
const rows = await db.query(sql);
// 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]
);
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.
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.
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.
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.
"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.
Threat modeling is not a document. It is a conversation, held early, with the people who know how the system actually works.
Sketch the data flows. Mark where data crosses a trust boundary: browser to API, service to service, your code to a third party.
Walk each boundary with STRIDE. Ask who could spoof, tamper, deny, leak or escalate here, and what they would gain.
Mitigate, accept, transfer or eliminate. Write down the decision and the owner so it survives the next reorg.
Turn each mitigation into a test. A threat model with no tests is a wish list.
| STRIDE | Violates | Typical question | Typical control |
|---|---|---|---|
| Spoofing | Authentication | Can someone pretend to be another user or service? | Strong auth, MFA, mutual TLS between services |
| Tampering | Integrity | Can data or code be altered in transit or at rest? | Signatures, integrity checks, server-side validation |
| Repudiation | Non-repudiation | Could a user deny doing something we cannot disprove? | Audit logs with identity, time and action |
| Information disclosure | Confidentiality | Where could data leak, including via errors and logs? | Encryption, minimal responses, access control |
| Denial of service | Availability | What happens under ten thousand times normal load? | Rate limits, quotas, timeouts, back-pressure |
| Elevation of privilege | Authorization | Can a low-privilege caller reach a high-privilege action? | Deny by default, check on the server, every time |
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
A reference you can keep open during code review. Open a section to see the practices that matter most in it.
Short, language-agnostic ideas shown in JavaScript. The principle carries to any stack.
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.
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);
});
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);
});
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.
app.get('/api/lookup/:personId', apiKeyAuth, async (req, res) => {
// unlimited calls, no baseline, no alert
res.json(await People.find(req.params.personId));
});
// 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 });
}
Binding the whole request body to a model is convenient, until someone adds "role": "admin" to the JSON.
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);
});
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 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.
| Stage | Control | What it catches | Sensible gate |
|---|---|---|---|
| Pre-commit | Secret scanning, linters | Credentials and keys about to enter the repository | Block the commit |
| Pull request | SAST, dependency review, required code review | Injection patterns, unsafe APIs, risky new dependencies | Fail on new high-severity findings |
| Build | SCA, SBOM generation, image scanning, license checks | Known-vulnerable libraries and base images | Fail on critical issues with an available fix |
| Test | DAST on an ephemeral environment, API fuzzing, authorization tests | Runtime flaws, broken access control, misconfigured endpoints | Fail on confirmed high-severity results |
| Release | Artifact signing, provenance attestation | Tampering between build and deploy | Block unsigned or unverified artifacts |
| Deploy | IaC scanning, policy-as-code, admission control | Public storage, privileged containers, over-broad IAM | Block policy violations |
| Run | Runtime monitoring, rate limits, logging, vulnerability management | Active exploitation, drift, abnormal usage | Alert, ticket, and meet the patch SLA |
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.
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.
Route issues into the same backlog as bugs, with severity, an owner and an SLA. An unowned dashboard is a graveyard, not a control.
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.
One interested developer per team, with a little time and training, scales security better than a central team trying to review everything.
Track time-to-remediate, share of services with scanning enabled, age of dependencies and repeat vulnerability classes. Avoid vanity counts of findings.
Compromising one popular package, one build step or one CI token can reach thousands of downstream applications. The defenses are unglamorous and effective.
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.
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.
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.
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.
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.
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.
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.
Every PurpleSecurity briefing has a lesson buried in it for people who build systems. Here are the ones that map most directly to code.
Millions of records pulled through a legitimate account over ten days, detected by an invoice. Rate limits and anomaly alerts are security controls, not performance settings.
Read the briefing →Stolen session cookies skip your second factor entirely. Short session lifetimes, device binding and token rotation are your side of the fix.
Read the briefing →Number matching, prompt rate limits and phishing-resistant factors turn a weak approve button into a real control. Authentication UX is a security feature.
Read the briefing →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
These are maintained by standards bodies and open communities. Prefer them over any blog post, including ours, for detail.
The standard awareness document for the most critical web application security risks.
owasp.org →Concise, practical guidance by topic: authentication, session management, SQL injection prevention and many more.
cheatsheetseries.owasp.org →A verification standard with testable requirements you can use to define and measure application security.
owasp.org →A maturity model to assess your software security practices and plan improvements in a structured way.
owaspsamm.org →A set of secure development practices that many procurement and compliance requirements now reference.
csrc.nist.gov →A framework for supply chain integrity, with levels that describe how trustworthy your build provenance is.
slsa.dev →Every briefing breaks down a real breach or attack technique, then gives you the one change worth making. Written for people who build and run systems.