Software Securityhome
Fix a findingSearch
Phase 4 · Testing

Automated security tests

Automated security tests are unit and integration tests that check a security control works: that a user cannot read another tenant's record, that a forbidden field is ignored, that a fixed vulnerability stays fixed. They live in the normal test suite and run on every change, like any other test.

Scanners do not know your rules. A missing ownership check on an invoice endpoint looks like ordinary code to SAST, and a DAST scan with one account cannot see cross-user access. Only a test that signs in as tenant A and asks for tenant B's invoice notices. The same tests stop a later refactor, or a generated rewrite, from quietly removing a control you already fixed once.

Who
The developers who build the feature, working from requirements and abuse cases agreed with AppSec or a security champion
When
While building the feature, alongside its functional tests, and whenever a vulnerability is fixed
How often
On every commit or pull request, with the rest of the test suite
You end up with
Tests for authorization, rejected input, limits and errors, each traced to a requirement, plus one regression test per fixed vulnerability

Do this

  1. List the controls each feature needs

    Start from the feature's security requirements (Security requirements) and abuse cases (Abuse cases): who may call each endpoint, which records and fields each role may read or change, which inputs are refused and which limits apply. OWASP ASVS 5.0.0 gives requirements you can cite by ID, such as v5.0.0-8.2.2 for data-specific access, and the OWASP Web Security Testing Guide has test ideas for each area.

  2. Build an authorization matrix

    For each endpoint, list the callers (anonymous, member, admin, admin of another tenant) and the expected result for each (200, 403 or 404), then run one test per cell from that table, as the OWASP Authorization Testing Automation Cheat Sheet describes. Seed two tenants and at least two roles, and change one fact at a time so a failure names the exact rule that broke.

  3. Test what must be refused

    Most security requirements say what must not happen, so test the refused case: another tenant's ID returns the same 404 as a missing record, PATCH /users/me with {"role":"admin"} leaves the role unchanged, an order with quantity: -1 is rejected, and confirming an unpaid order fails. NIST's minimum standard for developer verification (NISTIR 8397) lists these negative tests, of what the software should not do, among its techniques.

  4. Test limits, errors and logging

    Send 20 redemptions of a single-use coupon at the same instant and expect exactly one success, repeated over several rounds because a race does not show on every run. Force an unexpected exception and expect a generic message with no stack trace (v5.0.0-16.5.1). Request another tenant's record and expect a security log entry naming the user and the resource (v5.0.0-16.3.2).

  5. Add a regression test for every fixed vulnerability

    When a penetration test, bug report or scanner finds a real flaw, first write a test that reproduces it and fails, then fix the code and keep the test. NIST SSDF task PW.8.2 gives this as an example: incorporate tests for previously reported vulnerabilities into the test suite so they are not reintroduced. NISTIR 8397 calls them historical test cases. Fix a finding walks through the fix.

  6. Run them in CI and make them blocking

    Keep security tests in the same suite and the same required status check as the functional tests, not in a separate job run once before a release. Tag each test with the requirement it proves, for example with pytest's record_property, which adds a property to the test's JUnit XML entry, so a trace check can fail the build when an automated requirement has no passing test.

  7. Prove each test can fail

    On a branch, remove the control, such as the tenant filter, and confirm its tests fail; a test that still passes without the control proves nothing. Assert the response body, the stored row or the log entry, not only the status code. Review generated tests as carefully as generated code (AI coding assistants).

Done when

Security tests run in CI with the other tests and fail the build when a control regresses.

Mark it in the gap check

Common mistakes

  • Testing with one admin account. Most authorization bugs appear only between two tenants or two roles, so a single-user suite passes while the bug ships.
  • Testing only the happy path. A test that the owner can read an invoice says nothing about whether anyone else can.
  • Asserting only the status code. A 200 can still return fields the caller should never see, and a refused write can still change the stored row; assert the body and the data too.
  • Fixing a vulnerability without a regression test, so the next refactor can bring it back and nothing notices until the next penetration test or incident.
  • Running security tests in a separate nightly job nobody watches, instead of in the pull request where the author can fix the failure.
  • Mocking the layer under test, such as the authorization check, so the test passes whether or not the control exists.

Test cases by control

Starting points drawn from the site's abuse case library, each tied to an OWASP ASVS 5.0.0 requirement. Replace the routes and fields with your own.

Test cases by control: table
ControlTestExpected resultASVS 5.0.0
Function-level accessA member calls POST /invites directly403v5.0.0-8.2.1
Data-specific accessA user in tenant red requests an invoice owned by tenant blueThe same 404 as a missing invoicev5.0.0-8.2.2
Cross-tenant controlAn admin of tenant red sends an invite with tenant blue's tenant_id403, or the field ignoredv5.0.0-8.4.1
Mass assignmentPATCH /users/me with {"role":"admin"}Role unchangedv5.0.0-15.3.3
Input validationAn order with quantity: -1Validation error, nothing storedv5.0.0-2.2.1
Step orderConfirm an order whose payment failedRefused, and the order stays unpaidv5.0.0-2.3.1
Error handlingForce an unexpected exceptionGeneric message, no stack tracev5.0.0-16.5.1
Security loggingA refused read of another tenant's recordA log entry naming the user and the resourcev5.0.0-16.3.2

Tools and guides

Standards

NIST SSDF 1.1
PW.8 Test Executable Code to Identify Vulnerabilities and Verify Compliance with Security Requirements

Sources

  1. NIST SP 800-218: Secure Software Development Framework (SSDF) Version 1.1. February 2022.
  2. NIST IR 8397: Guidelines on Minimum Standards for Developer Verification of Software. July 2021, with October 2021 errata.
  3. OWASP SAMM: Requirements-driven Testing
  4. OWASP ASVS 5.0.0: V8 Authorization. Version 5.0.0.
  5. OWASP ASVS 5.0.0: V2 Validation and Business Logic. Version 5.0.0.
  6. OWASP ASVS 5.0.0: V15 Secure Coding and Architecture. Version 5.0.0.
  7. OWASP ASVS 5.0.0: V16 Security Logging and Error Handling. Version 5.0.0.
  8. OWASP Authorization Testing Automation Cheat Sheet
  9. OWASP Web Security Testing Guide. v4.2, the stable release; v5.0 in development.
  10. pytest: record_property and JUnit XML output

Find your next answer

Type to search. Search runs in your browser.

Searches run in your browser, but the host can infer them from the index files they load. The address shows your search so you can bookmark or share it.