Security and Threat Modeling
Your system is about to take its first real users or its first real data: a login page, a table of volunteer records, an API key for a paid service. This page covers how to find the threats that matter for that system, the vulnerabilities that turn up most often in web applications, and the baseline practices that close them: access control, authentication, secrets, dependencies, transport, and input handling, plus what changes when your system holds a dataset or a machine learning model. It doesn’t cover penetration testing, writing cryptography, or preparing for a compliance audit.
Security is the practice of building software that behaves correctly when someone is actively trying to make it misbehave. It comes from design decisions, such as who may see which data, and from build and deployment habits, such as where secrets live and which ports are open.
Threat Modeling: Thinking Like an Attacker
Section titled “Threat Modeling: Thinking Like an Attacker”The Threat Modeling Manifesto defines threat modeling as analyzing representations of a system to highlight concerns about its security and privacy, and frames it as four questions: “What are we working on?”, “What can go wrong?”, “What are we going to do about it?”, and “Did we do a good enough job?”
You’re looking for the threats that matter for your system, answered in proportion. A personal portfolio site and a healthcare data pipeline face different attackers, hold different assets, and deserve different amounts of effort.
A Lightweight Threat Modeling Process
Section titled “A Lightweight Threat Modeling Process”A formal threat model, with a dedicated tool and a review board, is more than a small team needs. Aim for a version the whole team can finish in one session, because the value comes from everyone working from the same picture of the system, and a session that short can be rerun when the architecture changes:
-
Draw the system. Sketch the components, the data flows between them, and the trust boundaries, where data crosses from one security context to another: from the browser to your API, or from your API to a third-party service. If you already have a C4 context or container diagram, start from it; otherwise, draw one.
-
Identify the assets. List the data and resources an attacker would want: user credentials, personal information, API keys, payment data, research datasets, administrative access.
-
Identify the entry points. List everywhere external input reaches the system: web forms, API endpoints, file uploads, URL parameters, WebSocket connections, configuration read from outside sources. Each one is part of the attack surface.
-
Ask what could go wrong at each entry point. For each entry point and asset, walk through the six threat categories of Microsoft’s STRIDE model:
Threat Question Example Spoofing Can someone pretend to be another user or system? Forged authentication tokens, session hijacking Tampering Can someone modify data they shouldn’t? Changing another user’s profile through a direct API call Repudiation Can someone deny an action they took? Deleted audit logs, unsigned transactions Information Disclosure Can someone see data they shouldn’t? Database dumps through SQL injection, error messages that expose internals Denial of Service Can someone make the system unavailable? Flooding an endpoint with requests, triggering expensive queries Elevation of Privilege Can someone gain access beyond their role? A regular user reaching admin endpoints, path traversal to read server files Don’t try to fill every cell. Rank the threats by how likely and how damaging each one is for your system, and carry the top of the list into step 5, because each threat you keep needs a response and, for a mitigation, someone to build it.
-
Decide what to do about each threat. The OWASP Threat Modeling Cheat Sheet gives four responses, from Adam Shostack:
- Mitigate: add a control that reduces the risk, such as input validation, authentication, or encryption.
- Eliminate: remove the feature or component that introduces the risk.
- Transfer: shift the responsibility to someone else, for example a managed authentication service instead of your own.
- Accept: leave the risk in place, because no other response fits the project’s requirements or constraints.
Record each decision with its reason. An Architecture Decision Record fits, because whoever later wants to undo an accepted risk needs to know why it was accepted. The same four responses apply to every risk in the project’s register.
-
Check the result. Read the list against the diagram. Every trust boundary should have at least one threat considered, and every Mitigate should have become a task with an owner, or the model changes nothing in the code.
The OWASP Top 10: Common Vulnerabilities
Section titled “The OWASP Top 10: Common Vulnerabilities”The OWASP Top 10 is a ranked list of the most critical security risks to web applications, published by the Open Worldwide Application Security Project (OWASP) and revised every few years. The current edition, Top 10:2025, ranks eight of its categories from testing data contributed for over 2.8 million applications and adds two that a community survey of practitioners voted in.
The ranking tells you where to look first. Broken Access Control is first: on average, 3.73% of the applications tested had at least one of its weaknesses. Security Misconfiguration moved up from fifth to second, and Injection fell from third to fifth. The sections below cover four of the ten, in rank order, with cross-site scripting under Injection.
Broken Access Control
Section titled “Broken Access Control”Access control decides what an authenticated user is allowed to do. It’s broken when a user can act on data or functions beyond their role:
- A user can view another user’s data by changing an ID in the URL (
/api/users/42/profileto/api/users/43/profile). - An API endpoint checks that a user is logged in but not that they may perform the requested action.
- Administrative functions are hidden in the interface but reachable through direct API calls.
Check authorization on the server, for every request, against the specific resource requested. Hiding a button or a route in the frontend doesn’t count, because anyone can send the request the button would have sent. The OWASP Authorization Cheat Sheet covers the patterns, including denying access by default and validating permissions on every request.
Security Misconfiguration
Section titled “Security Misconfiguration”Misconfiguration covers the settings left at a default or never changed for production:
- Debug mode left on in production, exposing stack traces and internal state.
- Default credentials on databases, admin panels, or cloud services.
- A CORS (cross-origin resource sharing) policy that lets any origin make authenticated requests.
- Cloud storage buckets left publicly readable.
- Ports or services exposed to the internet that nothing needs.
Each of these hands an attacker something for free: a stack trace maps your internals, a default password is one an attacker can look up, and an open bucket or port needs no exploit at all.
Check these before every deployment:
- Debug mode is off.
- Default passwords are changed.
- Secrets come from environment variables, not hardcoded values.
- CORS allows only the origins that need access.
- Error pages don’t leak stack traces or internal details.
The DevOps guide’s deployment checklist covers the rest of a release.
Injection
Section titled “Injection”An injection attack sends untrusted input to an interpreter (a database, a shell, a template engine) as part of a command or query, where it runs as code. SQL injection is the best-known variant:
# Vulnerable: user input is concatenated directly into the queryquery = f"SELECT * FROM users WHERE email = '{user_input}'"
# Safe: parameterized query separates data from codecursor.execute("SELECT * FROM users WHERE email = %s", (user_input,))Wherever input meets an interpreter, never build executable code by concatenating untrusted input. Use parameterized queries (prepared statements), as the OWASP SQL Injection Prevention Cheat Sheet details; it strongly discourages escaping input yourself.
The OWASP OS Command Injection Defense Cheat Sheet lists three primary defenses for shell commands. The first is to call a library function instead of a command where one exists (os.mkdir() rather than running mkdir). The second, escaping, blocks command injection but still lets an attacker pass one argument to the command. The third, for a command you can’t avoid, keeps the data separate from the command, such as passing the arguments as a list to Python’s subprocess.run without shell=True, and validates the command and its arguments against an allowlist.
An ORM (object-relational mapper) such as SQLAlchemy or Django’s parameterizes the queries it builds, so its standard queries are protected. Raw query methods and filters built from strings bypass that protection, and Django’s documentation on raw SQL warns you to pass user-controlled values as parameters every time you use it.
Cross-Site Scripting (XSS)
Section titled “Cross-Site Scripting (XSS)”The browser is an interpreter too, which is why XSS belongs here. An attacker gets a script into a page other users will view. If your application renders user-generated content without escaping it, that script runs with your site’s privileges and can steal session cookies, redirect users, or rewrite the page.
React, Vue, and Svelte escape the values you render by default; Vue’s security guide shows what that escaping does to an injected <script> tag. The risk comes back through each framework’s escape hatch for raw HTML: React’s dangerouslySetInnerHTML, Vue’s v-html, and Svelte’s {@html}. Use them only with content you fully control or have sanitized with a library like DOMPurify. Vue’s security guide also covers the contexts escaping doesn’t protect: URLs, style values, and inline JavaScript built from user input.
Authentication Failures
Section titled “Authentication Failures”Authentication verifies who a user is, and session management remembers that identity across requests. The common mistakes:
- Storing passwords in plaintext or with a fast general-purpose hash such as MD5 or SHA-256, which lets an attacker who steals the table test guesses quickly. The OWASP Password Storage Cheat Sheet recommends Argon2id, then scrypt, and bcrypt for legacy systems.
- Session tokens that are predictable, never expire, or survive logout.
- No rate limiting on the login endpoint, which leaves it open to brute-force guessing.
Don’t build your own authentication. Use your framework’s built-in system, such as Django’s, or a managed service such as Firebase Authentication or Auth0. That’s the Transfer response from the threat model: the provider carries password storage, the reset flow, and session handling, which are the parts that are easy to get subtly wrong. If you must write it yourself, treat it as your highest-risk component and test the failure paths (wrong password, expired session, logout) as carefully as the successful login.
Secrets Management
Section titled “Secrets Management”Secrets (API keys, database passwords, tokens, private keys) must never appear in your source code or its history. Keep them in environment variables loaded from a file Git ignores:
DATABASE_URL=postgres://user:password@localhost:5432/mydbAPI_KEY=sk-abc123...JWT_SECRET=your-secret-key.env.env.local.env.productionMeli et al. (2019) scanned public GitHub commits in near real time for almost six months, together with a snapshot covering 13% of public repositories, and found secret leakage in over 100,000 repositories, with thousands of new, unique secrets leaked every day, a count they present as a lower bound. A test string they pushed appeared in GitHub’s Search API results with a median delay of 20 seconds, and a single API key within GitHub’s rate limits saw about 99% of newly committed files matching their searches. The authors conclude that an attacker with minimal resources could compromise many GitHub users by stealing leaked keys.
If you’ve pushed a secret, rotate it first: generate a new credential and revoke the old one. GitHub’s guide to removing sensitive data makes revoking or rotating the first step and says rewriting history may not be needed once the credential is dead. Rewriting history doesn’t make a live credential safe: the same study recovered the full contents of commits removed with both git filter-branch and BFG, the two methods GitHub recommended at the time, from GitHub itself, given only the commit’s hash (Meli et al., 2019). Once the credential is revoked, leave the history alone; rewrite it only when what leaked can’t be revoked.
Outside your machine, secrets live in the platform’s secret store; read where each environment keeps them and how to set up .gitignore. The security decision is to give each environment its own credentials, so a key leaked from a laptop or a CI log doesn’t open production, and to give each credential only the access its environment needs.
Let GitHub watch for you as well. Secret scanning searches a repository’s Git history for credentials, and when it finds one issued by one of GitHub’s partner providers, GitHub notifies the provider so it can act, for example by revoking it. Push protection goes a step earlier and blocks a push that contains a recognized secret. Which repositories get each feature depends on the repository’s visibility, its owner, and the plan, and GitHub changes those rules, so read both pages and check your repository’s security settings before you rely on either. For a repository they don’t cover, run a pre-commit scanner such as gitleaks.
Dependency Security
Section titled “Dependency Security”Your project depends on dozens or hundreds of open-source packages, and a vulnerability in any of them is a vulnerability in your system. The 2025 OWASP Top 10 ranks Software Supply Chain Failures third: the category has the fewest occurrences in the testing data, which OWASP attributes to how hard it is to test for, but the highest average exploit and impact scores among the published vulnerabilities (CVEs) it analyzed.
Run your ecosystem’s audit command, which checks your dependencies against published vulnerability advisories: npm audit for Node.js, pip-audit for Python, and cargo audit for Rust.
npm audit # Node.jspip-audit # Python (install with: pip install pip-audit)cargo audit # Rust (install with: cargo install cargo-audit)On GitHub, turn on Dependabot alerts in the repository’s Settings, under Advanced Security. Dependabot then alerts you when a dependency on your default branch has a known vulnerability, and with Dependabot security updates enabled it also tries to fix each one with a pull request that updates the dependency.
Read each alert before you act on it. Check whether your code reaches the vulnerable function, and use the severity score, usually a CVSS score, to order the work. Don’t leave alerts open indefinitely, though, because a published advisory tells an attacker what to look for as clearly as it tells you.
HTTPS and Transport Security
Section titled “HTTPS and Transport Security”Every connection between your frontend and backend, between your own services, and between your backend and external services should use HTTPS. HTTPS encrypts data in transit, so nobody on the network path can read or modify it.
Managed hosting platforms issue and renew certificates for you, and on a virtual machine a reverse proxy terminates HTTPS in front of the application. If you run the proxy yourself, automate renewal, because an expired certificate either takes the site down or trains users to click through the browser’s warning. Caddy obtains and renews certificates from a public certificate authority such as Let’s Encrypt by default; Nginx needs its ACME module or a separate program that obtains certificates.
If a third-party API only supports plain HTTP, treat anything that passes through it as readable and modifiable by whoever sits on the network path.
Input Validation and Output Encoding
Section titled “Input Validation and Output Encoding”Injection and XSS share a cause: untrusted input treated as trusted code or data. The defense at the interpreter is passing input as parameters and encoding output. The other defense sits where input arrives: check that incoming data matches the expected format, type, length, and range before you process it, and reject anything that doesn’t. Do it on the server even if you also validate in the browser, because anyone can skip browser checks by sending requests straight to your API. The OWASP Input Validation Cheat Sheet covers allowlists and the cases where validation isn’t enough.
Security for Datasets and Models
Section titled “Security for Datasets and Models”Projects that handle datasets, machine learning models, or research data have concerns that a typical web application doesn’t:
- Data privacy: if your dataset contains personally identifiable information (PII), such as names, emails, health records, or locations, pseudonymize or anonymize it before processing and store the raw data with restricted access.
- Model security: a machine learning model can be attacked through its inputs, with evasion attacks that craft an input to force a wrong prediction, and through its training data, with poisoning. NIST AI 100-2 is NIST’s taxonomy of these attacks. If your model makes decisions that affect people, work out how someone could manipulate it.
- Notebook hygiene: a Jupyter notebook stores each cell’s outputs in the file next to its code (nbformat), so a cell that prints a connection string commits it. Clear outputs before committing, or strip them automatically with nbstripout, and read credentials from environment variables.
Best Practices
Section titled “Best Practices”- Decide the authorization model during design, and check authorization on the server for every request.
- Never commit secrets. Set up environment variables and
.gitignorebefore the first commit, and rotate any secret that reaches a remote. - Use your framework’s authentication or a managed service, and keep the framework’s defaults for session handling, password storage, cookie flags, and protection against cross-site request forgery (CSRF, where another site makes a logged-in user’s browser send a request the user didn’t intend). If you need to deviate, record why in an ADR so the next person can evaluate it.
- Validate all input on the server, even where the browser validates it too.
- Turn on Dependabot alerts and triage them as they arrive. Keep push protection on, and add a pre-commit secret scanner where GitHub’s secret scanning isn’t available.
- Check the OWASP Top 10 against your system before each major deployment, because each new feature can add an entry point the last check never saw.
- Use HTTPS for all network communication, including calls between your own services.
- Apply least privilege: give users, services, and API keys only the access they need, so a stolen key or a compromised account can do as little as possible.
- Log security-relevant events (failed logins, denied access, rejected input) so you can spot suspicious behavior; the OWASP Logging Cheat Sheet lists what to record and what to keep out of logs.
Some Truths About Security
Section titled “Some Truths About Security”For some systems a threat model is busywork, and it’s reasonable to skip it. A static site with no login, no user data, and no server-side code has little to model: its assets are the content and the credentials that deploy it, and the hosting platform runs everything else. Spend that session on those credentials and the dependency alerts instead. The model earns its time once the system holds accounts, personal data, or payments, or calls another service on a user’s behalf, because each of those adds a trust boundary that someone can try to cross.
The CWE Top 25 for 2025, which MITRE ranks from the weaknesses behind 39,080 published vulnerability records, opens with cross-site scripting, SQL injection, cross-site request forgery, and missing authorization, and the OWASP Top 10 opens with broken access control and misconfiguration. None of these needs a clever attacker, and OWASP publishes a cheat sheet on preventing each of the first four. Read one of the two lists carefully before you spend an afternoon on anything exotic, such as implementing cryptography yourself.
Treating security as all or nothing leads to a specific failure: a team decides it can’t really secure a small project, and so does nothing, trusting that nobody knows the system exists. Durumeric et al. (2013) built an open-source scanner that surveys the entire IPv4 address space in under 45 minutes from a single machine, and warn that scanning at that speed can uncover services their owners thought were well hidden. A database or an admin panel on a public address is found by whoever scans for its port. You won’t make your project unbreakable, but changed default credentials, closed ports, and authentication on every endpoint shut the doors that kind of scanning finds, and they’re within reach of any team.
Most fixes on this page can be applied late, one at a time. Authorization is the exception. Retrofitting authentication and access control into a system that assumed neither means revisiting every data path, because the question “who may see this row?” has to be answered in every query, endpoint, and view at once. Decide your authorization model during design, before the organization you’re building for asks whether volunteers can see each other’s details.
Knowing that passwords must be hashed doesn’t mean your code will hash them. Naiakshina et al. (2017) asked 20 computer science students to add registration and authentication to a social networking application. None of the 10 who weren’t told to store passwords securely did so, while 7 of the 10 who were told at least hashed them. Half the participants used the Spring framework, whose password-storage helpers were opt-in, and half used JSF, where they had to implement storage themselves; the authors conclude that easier APIs aren’t enough while their secure methods are opt-in, and that developers need to be asked for security explicitly. The study is small and its authors claim no generalizability, but the practical response is cheap: prefer the path where secure storage is already on (your framework’s built-in authentication, a managed provider), and when it isn’t, write the security requirement into the task so nobody has to remember it.
Experienced developers leak credentials too. Meli et al. (2019) compared the repositories that leaked secrets with a random sample of about 100,000 others and found that neither repository activity nor contributor experience was strongly correlated with leakage. One of their case studies was a set of AWS credentials for a major government agency’s website, committed by a developer who claims nearly 10 years of experience. Put the check in the tooling: a .gitignore entry for every secrets file before the first commit, push protection or a pre-commit scanner, and a rotation you’ve walked through once, so the first real leak isn’t the first time anyone tries it.
Security in Industry and Academia
Section titled “Security in Industry and Academia”In industry, threat modeling is part of published development processes. Microsoft’s Security Development Lifecycle has teams create threat models during design, keep them current as the software changes, and review every threat model for accuracy and completeness before a product is released. The AWS Well-Architected Framework recommends threat modeling to its customers as a security best practice and rates the risk of skipping it as high.
Regulation sets the minimum for some kinds of data. PCI DSS is written for every organization that stores, processes, or transmits cardholder data, and the GDPR can apply to a system that offers services to people in the European Union. If your system takes payments, a payment provider’s hosted checkout page keeps card numbers off your servers, which is the Transfer response applied to a compliance risk.
In academia, security research handles the same material it studies, and its practices carry over to any research project with sensitive data. Meli et al. (2019) collected over 200,000 leaked credentials. Their IRB found the work exempt because it used only public data, and they still ran every experiment from a locked-down virtual machine, kept the secrets inside the collection database, which was reachable only over key-authenticated SSH, and never used a discovered credential, not even to check that it worked.
References
Section titled “References”- Meli, M., McNiece, M. R., and Reaves, B. (2019). How Bad Can It Git? Characterizing Secret Leakage in Public GitHub Repositories. Network and Distributed System Security Symposium (NDSS). https://doi.org/10.14722/ndss.2019.23418
- Durumeric, Z., Wustrow, E., and Halderman, J. A. (2013). ZMap: Fast Internet-wide Scanning and Its Security Applications. 22nd USENIX Security Symposium. https://www.usenix.org/conference/usenixsecurity13/technical-sessions/paper/durumeric
- Naiakshina, A., Danilova, A., Tiefenau, C., Herzog, M., Dechand, S., and Smith, M. (2017). Why Do Developers Get Password Storage Wrong? A Qualitative Usability Study. Proceedings of the 2017 ACM SIGSAC Conference on Computer and Communications Security (CCS '17). https://doi.org/10.1145/3133956.3134082
Additional Readings
Section titled “Additional Readings”Sources and further reading
- OWASP Cheat Sheet Series: per-topic guidance on authentication, session management, file uploads, and much else not covered here.
- Application Security Verification Standard, OWASP: a list of security requirements to build and test against, for when the Top 10 isn’t detailed enough.
- Digital Identity Guidelines: Authentication and Authenticator Management, NIST SP 800-63B-4: the standard on passwords, including its rule against forcing periodic password changes.
- Known Exploited Vulnerabilities Catalog, CISA: vulnerabilities with evidence of exploitation in the wild, for deciding which alert to fix first.
- Security Engineering, Ross Anderson: a textbook on building systems that stay dependable under attack.
- DevSecOps Guideline, OWASP: adding security checks to a development pipeline.
- Have I Been Pwned, Troy Hunt: useful for showing a skeptical teammate what a breach corpus looks like.
Activities that exercise this
- Describe your Architecture: the diagram a threat model is drawn on top of.
- Designing for System Security: the design-time version of this guide.
- Apply the Privacy by Design (PbD) Guidelines: privacy decided up front rather than retrofitted.
- Audit Your Safety Net: whether your gates would catch a bad change before it merges.
- Risk Management Plan: where a security risk gets an owner and a date.