Pin the edition and requirement, such as v5.0.0-1.2.5, and the exact artifact and call sites in scope. For each requirement, record the hazard, the code that addresses it and a test another engineer can rerun, with expected and observed results. Keep pass, fail, not tested and not applicable apart. One passing path never outweighs a failing path in scope, and an approved exception does not turn a fail into a pass.
- ASVS 5.0.0 (May 2025) has 345 requirements in 17 chapters. Cite them with the version prefix, such as
v5.0.0-1.2.5: almost every 4.0.3 identifier changed. - Level 1 holds about 20% of the requirements, level 2 brings the total to about 70%, and level 3 adds the rest.
- Scope a record to an artifact, its call sites and an evidence period; one passing requirement is not a level.
- An argument list without a shell stops shell parsing but not the invoked program’s own option parsing.
- Not applicable needs a reason in the product; a missing test is “not tested”.
- A risk owner can accept a failing requirement for a time; the verification result stays fail.
- Search the artifact for every subprocess call and compare the result with the record’s call-site inventory.
- Run a harmless marker payload through each reachable route and confirm that no marker file appears.
- Confirm that the evidence names the artifact digest that is actually deployed.
- Check that every not-applicable result gives a reason in the product.
- Applies to
- OWASP ASVS 5.0.0, released 30 May 2025
Application verification records using a selected ASVS edition. Individual examples do not establish a complete ASVS level.
- Part of
- Learning path: Build an evidence-driven security program (step 2 of 11). Workflows: Review a design, Test an application, Build an AppSec program
Tested fixes: the vulnerable code, the fix, a plausible fix that still fails, and the regression tests that tell them apart.
Learning goals and prerequisites
What you will learn
- Use versioned ASVS identifiers and an explicit call-site boundary.
- Trace a technical requirement into implementation and behavioral evidence.
- Keep failed, partial, untested and accepted-risk states distinct.
Before you start
Security testers and engineers writing ASVS verification records
- Read a versioned requirement and distinguish a scoped test from a general security claim.
Refresh first: Review a code change through its trust boundaries
Go to the guided practiceASVS is a set of application verification requirements. The OWASP project identifies 5.0.0 as the stable release and recommends versioned requirement identifiers because numbering can change. A report saying ASVS tested without edition, level, applicable requirement set and application scope cannot be repeated. Record source revision, deployed artifact, environment, interfaces and exclusions. A single passing requirement is not an assessment of an entire ASVS level.
This guide uses v5.0.0-1.2.5, the OS command-injection requirement in chapter V1 (/asvs/5.0/v1/), as a concrete verification target. The explanation here paraphrases its intent: untrusted values must not become unintended operating-system command syntax. The scope is Cedar's thumbnail worker and its two subprocess call sites, not every service in the organization. The exercise supplies one safe-looking candidate and one overlooked legacy path. That makes coverage visible instead of letting one successful test stand for the whole application.
OWASP released ASVS 5.0.0 on 30 May 2025 with 345 requirements in the 17 chapters below. Each requirement has one level. Level 1 is the first layer of defense, 70 requirements or about 20%; most applications should aim for level 2, which brings the total to about 70%; level 3 adds the remaining 30% for applications that must show the highest assurance.
The numbering is new. Of the 286 requirements in version 4.0.3, only 11 survive unchanged, 109 were deleted, deduplicated or merged, and even unchanged requirements moved. A record that says “ASVS 4.0.3 V5.3.8” cannot be compared with a 5.0.0 result without remapping each requirement.
| Chapter | Name | Requirements |
|---|---|---|
| V1 | Encoding and Sanitization | 30 |
| V2 | Validation and Business Logic | 13 |
| V3 | Web Frontend Security | 31 |
| V4 | API and Web Service | 16 |
| V5 | File Handling | 13 |
| V6 | Authentication | 47 |
| V7 | Session Management | 19 |
| V8 | Authorization | 13 |
| V9 | Self-contained Tokens | 7 |
| V10 | OAuth and OIDC | 36 |
| V11 | Cryptography | 24 |
| V12 | Secure Communication | 12 |
| V13 | Configuration | 21 |
| V14 | Data Protection | 13 |
| V15 | Secure Coding and Architecture | 21 |
| V16 | Security Logging and Error Handling | 17 |
| V17 | WebRTC | 12 |
A useful verification argument names the hazard, mechanism and expected observation. For command injection, the hazard is that a user-controlled filename or label becomes shell syntax. A subprocess argument list with the shell disabled separates arguments from shell interpretation; it does not stop the invoked program from treating a filename that begins with a dash as an option. The program’s argument semantics and permitted inputs still need review.
The evidence chain therefore includes a call-site inventory, code and configuration review, and behavior tests. In the code below, Cedar’s S1 path runs a fixed executable with an argument list, no shell and a server-generated output name; the legacy S2 path builds a shell command by concatenating a user label. A happy-path thumbnail test exercises neither the dangerous label nor the legacy route. The assessor should identify the uncovered path and require a change plus regression before marking the scoped requirement satisfied; /fix/command-injection/ shows that fix and its test in several stacks.
import subprocess
import uuid
from pathlib import Path
CONVERT = "/usr/bin/convert"
OUTPUT = Path("/scratch/thumbnails")
def thumbnail_legacy(upload_path, label):
# S2, fails v5.0.0-1.2.5: the shell parses the user's label as command syntax.
subprocess.run(
f"{CONVERT} {upload_path} -resize 200x200 {OUTPUT}/{label}.png",
shell=True,
check=True,
)
def thumbnail(upload_path, convert=CONVERT):
# S1: fixed executable, argument list, no shell, and a server-generated output name.
# The label stays display metadata; it never reaches the command line.
target = OUTPUT / f"{uuid.uuid4().hex}.png"
subprocess.run(
[convert, str(upload_path), "-resize", "200x200", str(target)],
check=True,
timeout=60,
)
return targetA record should include requirement ID, applicability reason, test ID, artifact/source identity, environment, input, expected behavior, actual observation, evidence location, evaluator, time and result. Use pass, fail, not tested and not applicable as different outcomes. Not applicable needs a reason tied to product behavior; the absence of a test is not that reason. Preserve failing observations as well as the retest so reviewers can see what changed.
The fixture provides transcript E31, showing the legacy path created a harmless marker file in a disposable scratch directory, and E32, showing the argument-list path did not. These are fictional supplied observations, not results generated by this page. Their evidence IDs function as links within the training dossier. In a real report use immutable accessible records with sanitized inputs and outputs. Do not include live credentials, private customer data or an unverifiable screenshot as the sole proof. A test that cannot be located or tied to the artifact should be reported as missing evidence.
The /labs/release-evidence/ lab practices the same rule in code: test evidence from an older artifact must not decide the release of a newer one.
The result for the release is fail because one in-scope call site remains unsafe. S1 can be described as verified for the path tested; closure needs S2 fixed, both paths retested and the tested artifact identified. This is more useful than an overall percentage that hides a decisive failure. A risk owner may choose a bounded exception under organizational policy, but the technical verification result remains fail; acceptance does not turn the requirement into a pass.
Reports should distinguish requirement satisfaction from confidence and coverage. A carefully examined call-site inventory plus representative negative tests is stronger than one unexplained scanner result, yet it still depends on the inventory's completeness and environment fidelity. New subprocess calls, changed executable options or deployment configuration invalidate parts of the argument. Write those reopening conditions into the record. The purpose is a test another engineer can repeat and challenge, not a claim of certification from one laboratory example.
Guided practice
Assess one ASVS 5.0.0 requirement for Cedar's thumbnail worker, interpret an old unversioned report and challenge a not-applicable claim, using a two-call-site inventory and supplied harmless test transcripts.
Supplied scenario material
Target: ASVS 5.0.0 level 1 requirements for Cedar's thumbnail worker, release W7 (fictional).
Prior report R0 (2025, external vendor): "ASVS V5.3.8 pass; V14.2.1 pass; application is ASVS compliant."
No edition, scope, call sites or tests named.
Inventory I7: two OS command call sites in W7.
S1 upload path: fixed executable /usr/bin/convert, argument list, shell=False, server-generated output name.
S2 legacy batch re-render: shell=True; the user's label is concatenated into the command string.
E31 test T1 on W7/S2: a label with a shell separator and a harmless marker operation created /scratch/marker. Failed.
E32 test T2 on W7/S1: the same label treated as data; no marker; thumbnail produced. Passed.
Draft row: v5.0.0-5.2.1 "not applicable: the worker only accepts images". No upload size limit configured; no test.
Roles (fictional): assessor Ivo; remediation owner Mina. Evidence IDs refer to this dossier, not external files.1. Read the old report
Decide what R0's two results mean in ASVS 5.0.0 terms.
Expected result and reasoning
V5.3.8 is the ASVS 4.0.3 OS command injection requirement; v5.0.0-1.2.5 has the same text and is level 1, so the claim can be retested under the new ID. V14.2.1 can't be interpreted without an edition: in 4.0.3 it is keeping components up to date, in 5.0.0 it is keeping sensitive data out of URLs. And two requirements are not an ASVS level, so "ASVS compliant" says nothing testable.
2. Write the scope row
Create a versioned record for the command injection requirement.
Expected result and reasoning
ASVS v5.0.0-1.2.5 (level 1), Cedar thumbnail worker release W7, call sites S1 and S2 from inventory I7. Other services and the rest of ASVS are outside this record, and no level is claimed.
3. Evaluate the evidence
Assign the W7 result and explain why E32 doesn't change it.
Expected result and reasoning
W7 fails v5.0.0-1.2.5 on E31: the shell interprets the label in S2. E32 shows S1 is safe for the input tested, but one passing path can't outweigh a failing path in the same scope.
4. Challenge the not-applicable claim
Judge the draft row for v5.0.0-5.2.1.
Expected result and reasoning
Reject it. v5.0.0-5.2.1 asks that the application accepts only files of a size it can process without loss of performance or denial of service. Images are files with sizes, so the requirement applies; with no limit configured and no test, the honest result is not tested, with a likely fail to confirm.
5. Design the closure test
List the minimum evidence needed after correcting S2, and write the regression test that proves the shell no longer parses the label.
Pythonimport json import os import stat import sys import tempfile import unittest from pathlib import Path import thumbnails LABEL = "x; touch MARKER #" # harmless marker operation, as in transcript E31 class ThumbnailTest(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.root = Path(self.tmp.name) self.fake = self.root / "convert" # records its arguments instead of resizing self.fake.write_text( f"#!{sys.executable}\n" "import json, pathlib, sys\n" "pathlib.Path(sys.argv[-1]).write_bytes(b'png')\n" f"pathlib.Path({str(self.root / 'argv.json')!r})" ".write_text(json.dumps(sys.argv[1:]))\n" ) self.fake.chmod(self.fake.stat().st_mode | stat.S_IEXEC) thumbnails.CONVERT, thumbnails.OUTPUT = str(self.fake), self.root self.upload = self.root / "upload.jpg" self.upload.write_bytes(b"jpg") self.cwd = os.getcwd() os.chdir(self.root) def tearDown(self): os.chdir(self.cwd) self.tmp.cleanup() def test_legacy_call_runs_the_label_as_a_command(self): thumbnails.thumbnail_legacy(self.upload, LABEL) self.assertTrue((self.root / "MARKER").exists()) def test_fixed_call_passes_data_as_one_argument(self): odd_upload = self.root / "a;touch MARKER #.jpg" odd_upload.write_bytes(b"jpg") target = thumbnails.thumbnail(odd_upload, convert=str(self.fake)) self.assertFalse((self.root / "MARKER").exists()) self.assertTrue(target.exists()) argv = json.loads((self.root / "argv.json").read_text()) self.assertEqual(argv[0], str(odd_upload)) self.assertEqual(argv[1:3], ["-resize", "200x200"]) if __name__ == "__main__": unittest.main()Expected result and reasoning
Change S2 so the label never reaches a shell, then run a valid thumbnail and the harmless marker payload through both call sites in the fixed release, expect no marker and no unintended command, and record the artifact digest the tests ran on. Keep E31 as the original failure. The test below runs both paths against a fake converter: the legacy call creates the marker, and the fixed call passes a hostile file name as one argument.
6. Write the report sentence
Include the results, evidence, ownership and limits.
Expected result and reasoning
Cedar W7 fails v5.0.0-1.2.5 within the S1 and S2 scope: E31 shows command interpretation in S2, and Mina owns the fix, which Ivo retests. v5.0.0-5.2.1 is not tested. R0's results are not reused. No whole-application or ASVS-level claim is made.
The transcripts are fictional, and no command injection payload runs here. Results cover only the requirements and call sites in scope.
Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.
No draft saved on this device yet.
Repair the code
Release evidence lab: Keep stale test evidence out of a release decision
Sources & further reading
The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.
- OWASP ASVS 5.0.0 release. 30 May 2025.
- OWASP ASVS 5.0.0, V1 Encoding and Sanitization (requirement 1.2.5). Version 5.0.0.
- OWASP OS Command Injection Defense Cheat Sheet
- Python documentation: subprocess security considerations
- NIST SP 800-218: SSDF 1.1, final. Version 1.1 (February 2022).
Reference information
- SDLC phase
- TestingSecurity program
- ASVS 5.0.0 requirements
- v5.0.0-1.2.5