Write Security Requirements

Learn to write security requirements for features: define security expectations, apply a mental model, follow a step-by-step process, and complete a practical exercise. This lesson covers common pitfalls and what to study next in the Security foundations track.

Focus: write security requirements for features

Sponsored

When did you last ship a feature that worked exactly as planned, only to realize the password reset endpoint didn't enforce rate limiting, or the new export API exposed more fields than the user should see? If you're nodding, you already know the pain: security is treated as an afterthought, a list of generic rules that never quite map to what you actually built. This lesson gives you a practical, repeatable method for writing security requirements that fit your feature like a glove — not a buzzword salad, but clear, testable statements that prevent vulnerabilities before a single line of code is written.

The problem this lesson solves

You’re asked to build a new feature — a user profile, a file upload, a real-time dashboard. Requirements exist: what it does, how it looks, who uses it. But security? It’s often a footnote: “make sure it’s secure” or “follow best practices.” That’s not a requirement; it’s a wish.

Without explicit security requirements, you get a false sense of certainty. The team believes security is covered because everyone said “be secure.” In reality, the insecure design decisions happened long before code review — in assumptions about data, users, and threats.

The consequence: you ship features with default configurations, missing authorization checks, and unclear data retention. Security incidents then become fire drills, not prevented risks.

This lesson solves that by giving you a simple, brainstorming-to-boilerplate process to make security requirements concrete. You’ll learn to ask the questions that turn vague worries into implementable, testable, and reviewable acceptance criteria.

Core concept / mental model

Think of a security requirement as a contract between your feature and its environment. It states what must hold true under adversarial conditions: who can do what, what data is protected, how failures are handled.

Mental model — the security triangle: For any feature, you have three interacting elements:

  • Assets — data or resources the feature touches (user emails, files, API keys).
  • Threats — who or what could misuse those assets (attackers, accidental insiders, malicious insiders).
  • Controls — the mechanisms your feature must include to reduce risk (authentication, authorization, encryption, rate limiting).

A security requirement is essentially a control, stated in a way that is specific to the feature. Not “must encrypt data,” but “must encrypt user passwords with bcrypt using a work factor of at least 12.” Not “must check permissions,” but “must reject requests to /api/admin/* unless the request carries a valid JWT with role=admin.”

Definition: A security requirement is a clear, testable statement that defines how a feature must behave to protect its assets, meet compliance, and preclude common attack vectors.

Why this mental model? It guides you from abstract (“secure by design”) to concrete (“the OTP input must expire after 5 minutes and after 3 failed attempts”). It also forces you to name what you are protecting — you cannot write a good requirement if you cannot name the asset.

How it works step by step

Step 1 — Identify assets and data flows. List every piece of data the feature handles: input, storage, output, external calls. For each, note the risk level (public, internal, sensitive, restricted). This is the foundation — you cannot protect what you don’t inventory.

Step 2 — Enumerate threats. Ask “what could go wrong?” Use the CIA triad as a checklist:

  • Confidentiality — unauthorized disclosure of data.
  • Integrity — unauthorized modification or destruction.
  • Availability — feature becomes unavailable to legitimate users.

Also consider specific attacks: injection, broken authentication, sensitive data exposure, XML external entities (XXE), insecure deserialization, etc. The OWASP Top 10 is a handy checklist.

Step 3 — Define security objectives. For each threat that is realistic and impactful, write a short statement of what the system must do. E.g., “Prevent unauthorized users from accessing other users’ private messages.” That’s a security objective.

Step 4 — Turn objectives into requirements. For each objective, rewrite it as a testable requirement using formats like: “When X, the system must Y, so that Z” or “The system must prevent Y under condition X.” Make it concrete: specify crypto algorithms, timeouts, limit numbers, roles, input patterns.

Step 5 — Validate and prioritize. Not everything is critical. Use a simple risk matrix (probability × impact) to rank. State which requirements are mandatory (must have) vs desirable (should have) vs future (could have). This helps scoping.

Step 6 — Document and communicate. Requirements belong in your issue tracker, design doc, or feature spec — as acceptance criteria. Ensure they are visible to developers, testers, and reviewers.

That process is linear, but in practice you may bounce back — that’s normal. The key is to end with a list that is unambiguous.

Hands-on walkthrough

Let’s walk through a concrete example: a password reset feature. The user forgot their password; they input their email, receive a temporary link, set a new password. What are the security requirements?

Step 1 — Assets & data flows: user email, user ID, new password, temporary reset token (sent via email, stored Hashed). Email is PII; password is sensitive; token is high-value.

Step 2 — Threats: brute-force of token, link enumeration, token leakage in logs, password brute-force after reset, account takeover.

Step 3 — Security objectives: prevent token brute-force and enumeration, ensure only the intended email can reset, ensure strong password policy, prevent token reuse.

Step 4 — Write the security requirements:

REQ-1: Similarly to login, the password reset request must be rate-limited to 5 attempts per hour per account.
REQ-2: The reset token must be a cryptographically random 256-bit value, sent only in the email and stored hashed (SHA-256) in the database.
REQ-3: The reset link must expire after 15 minutes. After expiry, the token must be invalidated and the user must be able to request a new one.
REQ-4: The new password must be at least 12 characters and contain at least one uppercase, one lowercase, one digit, and one special character.
REQ-5: After a successful password change, all active sessions for the user must be invalidated.
REQ-6: The reset endpoint must not leak whether an email exists; it must return the same generic message for both existing and non-existing users.

Notice how these are testable: you can write automated checks for rate limits, token hashing, expiry, password rules, and session invalidation.

Step 5 — Implement and test. Here’s a small example in Python (Flask) that demonstrates how to apply these requirements:

from flask import Flask, request, jsonify
import hashlib, secrets, time
from datetime import datetime, timedelta

app = Flask(__name__)
# In-memory store for demo; use a real DB in production
reset_tokens = {}  # email -> (token, expiry)
rate_limits = {}   # email -> list of timestamps

PASSWORD_RULES = {
    'min_length': 12,
    'require_upper': True,
    'require_lower': True,
    'require_digit': True,
    'require_special': True
}

def validate_password(password: str) -> bool:
    if len(password) < PASSWORD_RULES['min_length']:
        return False
    if PASSWORD_RULES['require_upper'] and not any(c.isupper() for c in password):
        return False
    if PASSWORD_RULES['require_lower'] and not any(c.islower() for c in password):
        return False
    if PASSWORD_RULES['require_digit'] and not any(c.isdigit() for c in password):
        return False
    if PASSWORD_RULES['require_special'] and not any(c in '!@#$%^&*' for c in password):
        return False
    return True

def generate_reset_token():
    return secrets.token_urlsafe(32)  # 256 bits of entropy

def hash_token(token: str) -> str:
    return hashlib.sha256(token.encode()).hexdigest()

def is_rate_limited(email: str, limit=5, window=3600) -> bool:
    now = time.time()
    rate_limits.setdefault(email, [])
    rate_limits[email] = [t for t in rate_limits[email] if now - t < window]
    if len(rate_limits[email]) >= limit:
        return True
    rate_limits[email].append(now)
    return False

@app.route('/request-reset', methods=['POST'])
def request_reset():
    email = request.json.get('email')
    # Always return the same message to prevent enumeration
    if not email:
        return jsonify({'detail': 'If the email exists, a reset link has been sent.'}), 200
    if is_rate_limited(email):
        return jsonify({'detail': 'Too many attempts. Try later.'}), 429
    token = generate_reset_token()
    expiry = datetime.now() + timedelta(minutes=15)
    reset_tokens[email] = (hash_token(token), expiry)
    # Send email with token (omitted)
    return jsonify({'detail': 'If the email exists, a reset link has been sent.'}), 200

@app.route('/reset-password', methods=['POST'])
def reset_password():
    email = request.json.get('email')
    token = request.json.get('token')
    new_password = request.json.get('new_password')
    if email not in reset_tokens:
        return jsonify({'detail': 'Invalid or expired token.'}), 400
    stored_token, expiry = reset_tokens[email]
    if datetime.now() > expiry:
        del reset_tokens[email]
        return jsonify({'detail': 'Invalid or expired token.'}), 400
    if hash_token(token) != stored_token:
        return jsonify({'detail': 'Invalid or expired token.'}), 400
    if not validate_password(new_password):
        return jsonify({'detail': 'Password does not meet requirements.'}), 400
    # Update password (omitted) and invalidate sessions (omitted)
    del reset_tokens[email]
    return jsonify({'detail': 'Password updated.'}), 200

if __name__ == '__main__':
    app.run()

Expected behavior: rate limiting on /request-reset, generic messages for non-existent email, 15-minute token expiry, SHA-256 hashing of the token, and password policy enforcement.

Now, let’s create a small test that verifies one requirement:

import requests

BASE = 'http://localhost:5000'

def test_token_expiry():
    # request token
    r = requests.post(BASE + '/request-reset', json={'email': 'test@example.com'})
    assert r.status_code == 200
    # Simulate token creation, then tamper with expiry to -1 minute (for demo)
    from app import reset_tokens, datetime, timedelta
    email = 'test@example.com'
    token, _ = reset_tokens[email]
    reset_tokens[email] = (token, datetime.now() - timedelta(minutes=1))
    r = requests.post(BASE + '/reset-password', json={'email': email, 'token': 'abc', 'new_password': 'StrongPass123!'})
    assert r.status_code == 400
    print('Expired token test passed.')

test_token_expiry()

This test simulates an expired token and expects rejection. In a real project you’d have unit and integration tests for every requirement.

Step 6 — Document all requirements in your issue tracker as acceptance criteria, e.g., [REQ-1] Rate limit password reset requests.

Compare options / when to choose what

When writing security requirements, you have several formats and levels of granularity. Here’s a comparison:

Approach Pros Cons When to use
Freeform narrative (“must be secure”) Quick, easy to write Vague, hard to test, ambiguous Only as a high-level objective; never as a requirement
Acceptance-criteria style (“When X, system must Y so that Z”) Testable, concrete, whipersandbox Takes time and thought Ideal for any feature with security impact
Security stories (“As a user, I want … so that my data is protected”) User-centric, ties to value Can fall into vague language Useful for early exploration; needs refinement
Automated policy as code (e.g., Rego/OPA, or code-based tests) Machine-checked, prevents drift Requires tooling and maintenance When you have CI/CD and compliance needs

For most teams, acceptance-criteria style is the sweet spot. It’s unambiguous, easy to review, and directly feeds into test automation.

Troubleshooting & edge cases

  • Vague requirement: “The system must be secure” — rewrite as a testable statement: “The login form must use HTTPS and implement account lockout after 5 failed attempts for 15 minutes.”
  • Overengineering: Requirements that demand extremely complex controls for low-risk assets — prioritize by risk; a hobby project may not need multi-factor auth for every change.
  • Missing edge cases: Forgot to cover concurrent sessions, token reuse after expiry, or race conditions on the rate limit — think about sequences and states.
  • Wrong error messages: Returning “Invalid email” vs “Invalid email or password” — write requirements to always return generic messages to prevent user enumeration.
  • Scope creep: Feature teams sometimes confuse security requirements with security hardening of the entire platform — keep requirements feature-specific; the platform has its own.
  • Assumption of “authentication is handled elsewhere”: Even if your app has auth, your feature may need authorization — state who exactly can access the feature and what they can do.
  • Password requirements too strict or too weak: Balance security with usability; often NIST guidance suggests length > complexity, so allow 14+ character passwords and support passphrases, while requiring at least 8 chars.

What you learned & what's next

You learned to write security requirements for features by identifying assets, threats, and controls, and turning them into clear, testable statements. You can now:

  • Explain the core idea: each requirement is a contract between the feature and its security environment.
  • Apply a step-by-step process: asset inventory → threat enumeration → security objectives → testable requirements → prioritization.
  • Complete a practical exercise: you wrote and implemented security requirements for a password reset flow.

You also saw common pitfalls like vague wording, missing edge cases, and user enumeration.

What’s next: In the next lesson, you’ll learn how to threat model your own features systematically — you’ll apply techniques like STRIDE and data flow diagrams to uncover deeper security risks early in the design phase. That’s the natural next step to refine your ability to predict and prevent vulnerabilities.

Practice recap

Pick an existing feature in your project (e.g., a file upload API). List its assets, identify three realistic threats, and write at least three security requirements in acceptance-criteria format. Then add them as acceptance criteria in your issue tracker and write a unit test for one of them. This will reinforce the process and make your next feature inherently more secure.

Common mistakes

  • Writing vague security requirements like 'must be secure' instead of stating what exactly to protect and how.
  • Forgetting to include requirements for error messages, leading to user enumeration vulnerabilities.
  • Over-specifying on low-risk features while ignoring critical areas.
  • Not covering edge cases like token reuse, expiry, or concurrent sessions.

Variations

  1. Security acceptance criteria style (When X, system must Y) — testable and concise.
  2. Security user stories (As a … I want … so that …) — user-centric but may need refinement.
  3. Automated policy as code (e.g., OPA) — machine-checked but requires tooling.

Real-world use cases

  • E-commerce site: defining PCI-DSS requirements for credit card data handling in the checkout feature.
  • Healthcare app: writing HIPAA-compliant auth and audit requirements for patient record access.
  • Fintech API: specifying OAuth scopes and rate limiting for account access endpoints.

Key takeaways

  • Security requirements are testable contracts that specify asset protection under adverse conditions.
  • Use the CIA triad (confidentiality, integrity, availability) to enumerate threats.
  • Follow a 6-step process: assets → threats → objectives → requirements → validation → documentation.
  • Write requirements in an acceptance-criteria format (When X, system must Y) to enable test automation.
  • Prioritize by risk using a simple probability×impact matrix.
  • Always include error handling requirements to avoid information leakage.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.