Free security template

Threat model checklist

STRIDE questions for one feature as data, with tests that every threat has an owner and a mitigation.

It opens with this template loaded and runs in your browser. You don't need an account to use it; a free account saves your work and exports it.

The job it does

When a feature touches logins, money or personal data: before it ships, and again when it changes. It comes with its own checks, so after you change it you can see at a glance that it still does the job.

What's inside

This is the template exactly as the Security Lab opens it, read-only. Open it to run it, change it and export it.

  • 3 files
  • Runs from checklist.js
  • JavaScript
The brief: Threat model: partner orders

When a feature touches logins, money or personal data, model the threats before it ships, and again when it changes. The model is data in threats.js (one row per threat, by STRIDE category, with a mitigation, an owner and a status), so it can be reviewed in a pull request and kept with the code.

The tests are the review checklist: every STRIDE category considered, every threat owned and mitigated, every high-risk one done before release. Run prints the table for the design doc. Replace the feature and the threats with yours.

Done when
  • All my tests pass
  • Output contains “6 of 6 STRIDE categories covered, 0 gaps”
  • No uncaught errors
threats.js
// Threat model for one feature, as data you can review and keep with the code.
// STRIDE: Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege.
export const FEATURE = {
  name: 'Partner shops order through the app',
  assets: ['partner accounts', 'orders and prices', 'credit limits'],
  entryPoints: ['POST /partner/orders', 'partner login', 'order CSV upload'],
};

export const THREATS = [
  { id: 'T1', stride: 'S', threat: 'Someone logs in as a partner with a stolen password', risk: 'high',
    mitigation: 'MFA for partner accounts; lock after 10 failures', owner: 'Identity team', status: 'done' },
  { id: 'T2', stride: 'T', threat: 'A partner changes the unit price in the order request', risk: 'high',
    mitigation: 'Prices come from the server catalogue, never from the request', owner: 'Orders team', status: 'done' },
  { id: 'T3', stride: 'R', threat: 'A partner says they never placed an order', risk: 'medium',
    mitigation: 'Audit log of who ordered what, from which device, kept 2 years', owner: 'Orders team', status: 'planned' },
  { id: 'T4', stride: 'I', threat: 'One partner reads another partner\'s orders by changing the id', risk: 'high',
    mitigation: 'Every query filters by the signed-in partner (tested)', owner: 'Orders team', status: 'done' },
  { id: 'T5', stride: 'D', threat: 'A huge CSV upload ties up the order service', risk: 'medium',
    mitigation: 'Upload limit 2 MB and 5,000 rows; processed by a queue', owner: 'Platform team', status: 'planned' },
  { id: 'T6', stride: 'E', threat: 'A partner raises their own credit limit through the API', risk: 'high',
    mitigation: 'Credit limits only change in the back office, with a second approver', owner: 'Finance systems', status: 'done' },
];
checklist.js
import { THREATS } from './threats';

export const STRIDE = { S: 'Spoofing', T: 'Tampering', R: 'Repudiation', I: 'Information disclosure', D: 'Denial of service', E: 'Elevation of privilege' };

/** Threats that are not ready for review: no mitigation, no owner, or high risk and not done. */
export function gaps(threats) {
  return threats.filter((t) => !t.mitigation || !t.owner || (t.risk === 'high' && t.status !== 'done'));
}

/** STRIDE letters with no threat written down yet. */
export function uncovered(threats) {
  return Object.keys(STRIDE).filter((k) => !threats.some((t) => t.stride === k));
}

/** The model as a Markdown table, to paste into the design doc. */
export function toMarkdown(threats) {
  const rows = threats.map((t) => '| ' + [t.id, STRIDE[t.stride], t.threat, t.risk, t.mitigation, t.owner, t.status].join(' | ') + ' |');
  return ['| ID | STRIDE | Threat | Risk | Mitigation | Owner | Status |', '|---|---|---|---|---|---|---|', ...rows].join('\n');
}

const covered = Object.keys(STRIDE).length - uncovered(THREATS).length;
console.log(toMarkdown(THREATS));
console.log(covered + ' of 6 STRIDE categories covered, ' + gaps(THREATS).length + ' gaps');
checklist.test.js
import { THREATS } from './threats';
import { gaps, uncovered } from './checklist';

test('every STRIDE category has at least one threat', () => {
  expect(uncovered(THREATS)).toEqual([]);
});
test('every threat has a mitigation and an owner', () => {
  expect(THREATS.filter((t) => !t.mitigation || !t.owner).map((t) => t.id)).toEqual([]);
});
test('every high-risk threat is mitigated before release', () => {
  expect(gaps(THREATS).map((t) => t.id)).toEqual([]);
});
test('gaps() catches a high-risk threat that is only planned', () => {
  expect(gaps([{ id: 'X', stride: 'S', risk: 'high', mitigation: 'm', owner: 'o', status: 'planned' }])).toHaveLength(1);
});

The lesson behind it

Learn why it works, with a podcast and a quiz for each lesson.

  • Password policy checker

    Length, breached-list and repeat checks in one function, with tests for the policy you agree.

  • Log triage

    Parse auth log lines, group failures by account and address, and flag brute force and spraying.

All security templates · Every template