Web, API, SaaS, and network field manual
Original research cutoff: 11 September 2026; targeted content review: 14 September 2026. Commands below are examples, not scans performed during this research. Operational budgets and report thresholds are recommended field practice; each program's written policy overrides them.
Investigation modules#
Use these procedures with this playbook's setup and scope. Each contains the full testing sequence, command cheat sheet, interpretation, and checklist; open its canonical page for cross-target references.
Object, function, and property authorizationTurn identities, tenants, objects, roles, and lifecycle transitions into a reproducible permission matrix.
Open canonical module and reverse references
- 01Establish permission rule
- 02Seed two controlled identities
- 03Replay one changed dimension
- 04Verify durable effect
- 05Revoke and repeat
- 06Record smallest violation
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and invariant
Use accounts A and B, two private tenants, and one synthetic document per tenant. Create a viewer role only if the product supports it. Record the exact build, API version, actor, tenant membership, document visibility, and expected permission rule. The invariant is: the server authorizes the actor, action, object, and each protected property together. UI visibility is supporting context; product policy establishes the boundary. Horizontal, vertical, and context-dependent authorization are separate dimensions. Access-control models.
Numbered test procedure
- Capture a working baseline. As A, create private O1 containing
BB-A-ONLY-01; as B create O2 containingBB-B-ONLY-01. Save each normal read response and a normal reversible rename. Keep separate browser profiles and credentials. Confirm anonymous access to O1 fails. - Test the object dimension. Replay A's O1 read using B's credentials, changing nothing else. Then keep B's identity and replace O1 with O2 as a positive control. Substitute only known controlled identifiers, including parent tenant IDs, child IDs, attachment IDs, and export-job IDs. A nonexistent synthetic ID is the negative control for error handling; it does not replace the cross-account test.
- Test the function dimension. Give B a documented viewer role in A's test tenant. Capture an owner-only operation as A, such as renaming a private test folder, and replay it as B. If the product permits viewer renames, select an actually restricted operation. Inspect durable state from A's browser after the request completes.
- Test the property dimension. Start with a permitted profile update. Add exactly one known server-controlled property from schema or source, such as a role field in the isolated tenant. Read back the stored object, then test one corresponding restricted action. Repeat separately for unauthorized response properties. Automatic binding of request fields can create mass-assignment issues; read-side property disclosure is a different mechanism. OWASP mass assignment.
- Test lifecycle and alternate delivery. Remove B's membership, replay the previously successful request, and check an existing subscription or queued export. Repeat the same permission rule at list/search, download, batch, and versioned endpoints where present. Use two-item batches containing O1 and O2; do not enumerate identifiers.
- Interpret and clean up. Match markers, object IDs, committed changes, and resulting permissions. A 200 with an empty list, a 202 for a rejected job, or a redirect without private content is not proof. Remove synthetic records and memberships after preserving minimal evidence.
Command card
These illustrative routes require your local replica. Create A.header and B.header containing each test account's complete Authorization: Bearer … header; protect the files and exclude them from reports. curl can load headers from a file. curl manual.
mkdir -p evidence/authz
chmod 600 A.header B.header
curl -q --noproxy '*' --max-time 10 --header @A.header \
-D evidence/authz/a.h -o evidence/authz/a.json \
http://127.0.0.1:8000/api/documents/O1
curl -q --noproxy '*' --max-time 10 --header @B.header \
-D evidence/authz/b.h -o evidence/authz/b.json \
http://127.0.0.1:8000/api/documents/O1
curl -q --noproxy '*' --max-time 10 --header @B.header \
-o evidence/authz/control.json \
http://127.0.0.1:8000/api/documents/O2
Report threshold and checklist
Report a demonstrated unauthorized read, action, or protected-property change. Attach the intended rule, both actors' roles, minimal request difference, and independent state verification. UUID disclosure, shared objects, intentionally public exports, stale UI state, and identical status codes are common false positives.
- Ownership and visibility confirmed before testing.
- Object, function, and property decisions tested separately.
- Positive and negative controls captured.
- Background output and revoked membership checked.
- No unrelated customer records accessed.
Local curl examples use --noproxy '*' to bypass inherited proxy variables; -q alone only disables the default curl configuration file. Configure intentional interception separately. curl manual.
Authentication, recovery, federation, and session lifecycleTest registration, recovery, identity changes, invitations, SSO enforcement, and deprovisioning as one account state machine.
Open canonical module and reverse references
- 01Map identity states
- 02Capture valid and invalid controls
- 03Alter one binding
- 04Verify account identity
- 05Test revocation
- 06Restore account settings
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and invariant
Prepare two controlled accounts, separate inboxes, browser profiles, and a recovery route for each. Use an isolated identity provider for federation modifications. Write the accepted identity states: unverified, password accepted, MFA pending, fully authenticated, elevated, recovered, and revoked. The invariant is: each privileged transition requires the right identity proof, and credentials remain bound to their intended account and purpose. Consult documented expiry and revocation semantics before judging residual access. OWASP authentication.
Numbered test procedure
- Inventory entry points. Record registration, email verification, password login, passkey/MFA completion, mobile login, SSO, API-token issuance, email change, linking, reset, and logout. Capture one complete normal sequence with timestamps and account identifiers. An anonymous call to
/api/meand an intentionally invalid credential form negative controls. - Check incomplete authentication. Stop A's normal login immediately before MFA. Use that intermediate cookie or token for a single protected profile read. Compare it with a completed A session and an anonymous request. Also check the observed alternate API route; avoid guessing or spraying credentials. Protected access without the required second factor is the finding, not the mere existence of an intermediate token.
- Check recovery binding. Request one reset for A and one for B. In the local replica, submit A's reset proof with B's account selector, if that selector is client-controlled. After an authorized normal reset of A, replay the consumed proof once. Confirm which controlled account's credential changed by a fresh login. Restore passwords afterward. Single-use, expiry, and account binding are independent checks. OWASP recovery.
- Check session transitions. Save the pre-login identifier, completed session, and any refresh credential separately. Compare identifiers after login and elevation. Perform logout or explicit device revocation and make one old-session profile request. Record access and refresh outcomes separately; a refresh token being revoked does not necessarily revoke already-issued access tokens. Evaluate the actual advertised security behavior. OWASP sessions.
- Check identity changes and pre-existing state. Change A's verified email to a second owned inbox; compare verification, pending state, and recovery delivery before/after confirmation. In the replica, keep an unverified password account open, then complete the legitimate verified/federated signup for the same owned inbox in another profile. Inspect whether the original password, session, or linked identity can still access the now-verified account. Independently test reusing a stale verification link after another email change. A duplicate signup error or two aliases reaching the same mailbox proves no takeover. Do not assume case folding, dot removal, or plus-address handling is universal. Email identity comparison and verification.
- Follow enterprise onboarding and offboarding. Use a disposable tenant with a second recovery administrator. Record invitation recipient/role/tenant, acceptance, cancellation, SSO enforcement, group assignment, and deactivation. Compare a fresh invite with one cancelled invite; confirm whether a changed recipient is allowed by the product contract. After enabling enforced SSO, attempt the observed password, reset/magic-link, and alternate client entry paths once with controlled accounts, then request one protected tenant object. Deliberate break-glass exceptions are separate policy. For SCIM, use the tenant's provisioning credentials and verify deactivation, group removal, token refresh, and existing streams after the configured sync interval. SCIM
activehas provider-defined semantics; it is not a universal immediate-session-kill instruction. SCIM schema. Provisioning credentials must still map to the permitted tenant/resources. SCIM authorization. - Follow the specialized proof branches. Use MFA/passkey lifecycle for enrollment, factor replacement, backup-code reuse, and WebAuthn verifier checks. Use federation/token trust for OAuth PKCE, OIDC claim and account-linking decisions, JWT validation, and SAML. Do not substitute decoded claims for a protected-resource result. Document which providers, authentication methods, and enterprise features were unavailable rather than marking them passed.
Command card
Export A's local cookie jar before using the application's real logout button. These requests compare protected content before and after logout; routes and credential transport must match the replica.
mkdir -p evidence/session
chmod 600 A-before.cookies
curl -q --noproxy '*' --max-time 10 -b A-before.cookies \
-D evidence/session/before.h -o evidence/session/before.json \
http://127.0.0.1:8000/api/me
# Perform the actual logout or device-revocation workflow now.
curl -q --noproxy '*' --max-time 10 -b A-before.cookies \
-D evidence/session/after.h -o evidence/session/after.json \
http://127.0.0.1:8000/api/me
curl -q --noproxy '*' --max-time 10 -o evidence/session/anonymous.json \
http://127.0.0.1:8000/api/me
Report threshold and checklist
Report access as the wrong controlled identity, completion without required authentication, unauthorized account linking, or a material violation of revocation guarantees. Cosmetic logout, unsent browser cookies, decoded JWT contents, and reset forms displaying without completing are common false positives.
- Account and session state recorded for every request.
- Browser binding distinguished from token validity.
- Correct and incorrect proofs compared independently.
- Fresh login verifies any credential or linking change.
- Verification, linking, invitations, and enforced-SSO fallback paths evaluated.
- Provisioning changes checked against configured sync and revocation behavior.
- Recovery settings restored; live tokens removed from report.
Trace input into queries, commands, and templatesProve interpretation at a specific sink using small paired experiments instead of payload spraying.
Open canonical module and reverse references
- 01Identify input and sink
- 02Freeze a synthetic fixture
- 03Send paired controls
- 04Observe sink semantics
- 05Confirm smallest effect
- 06Document root cause
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and invariant
Use a local replica seeded only with synthetic records, a normal request, and an observable sink: database query logs, a template render, or a disposable worker's process log. Identify input type and decoding layers before selecting syntax. The invariant is: untrusted input remains data, or is constrained to an explicitly authorized language. A SQL search field, MongoDB filter, template editor, and process argument have different trust contracts; the same string is not a universal test.
Numbered test procedure
- Trace the route. From the handler, follow validation, normalization, service calls, query builders, template compilation, and subprocess invocation. In a black-box target, infer this from observed feature behavior and errors, then reproduce locally when possible. Record the last point where input is still data and the first interpreter boundary. An ORM can still contain unsafe raw fragments.
- Freeze controls. Create exactly one matching synthetic record and one nonmatching record. Capture the normal response. Choose an input that must produce no match under the intended semantics. Keep actor, tenant, pagination, and cache state unchanged across each pair.
- Test SQL or NoSQL semantics. For a known SQL string context in the lab, compare logically true and false expressions while retaining the same synthetic predicate. Prefer local query logs over timing or data extraction. For a JSON filter expected to accept a string, compare
{"name":"BB-CANARY"}with{"name":{"$eq":"BB-CANARY"}}, then$eqwith a nonexistent marker. Operator acceptance becomes a vulnerability when it escapes the allowed query contract or security predicate; intentional query APIs are different. SQL injection, NoSQL injection. - Test template evaluation. For a documented Jinja/Twig-like local sink, compare literal text,
{{7*7}}, and{{7*8}}. Inspect the raw HTTP response before JavaScript executes. Results 49 and 56 support server interpretation; a single 49 might be ordinary content. If the feature intentionally supports expressions, determine whether a lower-trust actor crosses a promised sandbox or data boundary before reporting. Arithmetic alone does not establish OS execution. Template injection. - Test command interpretation locally. With an identified POSIX shell sink, use a disposable process and a marker-only operation such as
printf BB-CMD-01; compare the literal argument path with the shell-interpreted path in process logs. Avoid file writes, network callbacks, account enumeration, and long sleeps. Also distinguish argument injection into a legitimate executable from starting an additional shell command. Command injection. - Confirm causality and clean up. Repeat the baseline and one successful pair, then switch to the validated/parameterized route in the replica as a negative control. Capture the input-to-sink path, raw values after decoding, and smallest observable impact. Remove fixtures and stop at proof.
Command card
The following performs three bounded template probes against an illustrative local endpoint. Use only when that endpoint is expected to render the parameter as text; do not infer engine identity from one expression.
mkdir -p evidence/injection
curl -q --noproxy '*' --max-time 10 --get --data-urlencode 'name=BB-LITERAL' \
-o evidence/injection/baseline.txt \
http://127.0.0.1:8000/preview
curl -q --noproxy '*' --max-time 10 --get --data-urlencode 'name={{7*7}}' \
-o evidence/injection/expression-a.txt \
http://127.0.0.1:8000/preview
curl -q --noproxy '*' --max-time 10 --get --data-urlencode 'name={{7*8}}' \
-o evidence/injection/expression-b.txt \
http://127.0.0.1:8000/preview
Report threshold and checklist
Report attacker-controlled interpretation that violates the feature's contract, supported by paired results or an instrumented source trace. Syntax errors, WAF blocks, reflection, slow requests, and intentionally supported expression syntax are insufficient alone. State whether the demonstrated impact is query manipulation, expression evaluation, argument injection, or command execution.
- Sink, language, quoting context, and decode order identified.
- Synthetic positive and negative records available.
- One input changed per comparison.
- Raw server output separated from client rendering.
- Evidence stops short of secret retrieval or destructive execution.
Local curl examples bypass environment proxies with --noproxy '*'; -q alone only suppresses curl configuration files. These snippets target loopback and do not follow redirects. curl manual.
Delayed consumers and argument-only execution
A safe database insert does not make the stored value trusted forever. In the replica, save a marker through the normal create/update route, then trigger one observed export, search, scheduled job, or administration view that consumes it. Capture both requests, the stored bytes, and the later interpreter call. Repeat with a literal marker and one context-specific expression; keep the actor and selected synthetic record constant. This checks second-order injection that an immediate-response scan misses. Second-order SQL injection.
Treat process arguments as a separate boundary. A subprocess argument array with shell=False can still pass an attacker-controlled option to the intended executable. In a local wrapper, compare a normal filename-like argument with a harmless, documented mode switch and observe the exact argument vector and output. Prove that the caller was allowed to supply a value but not that mode. Do not test options that write files, run another program, or contact a network. Report shell-command interpretation under CWE-78 and argument/option injection under CWE-88; neither automatically proves the other. CWE-88.
- Stored input followed through its later privileged consumer where one exists.
- Argument vector distinguished from shell command text.
Server-side fetching and network trust boundariesSeparate an intended fetch from a request that reaches a prohibited destination, forwards authority, or exposes a protected response.
Open canonical module and reverse references
- 01Map fetch contract
- 02Start controlled canary
- 03Confirm ordinary fetch
- 04Change one destination rule
- 05Correlate server evidence
- 06Bound the impact
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and invariant
Identify a feature that causes server-side requests: preview, import, webhook verification, document conversion, image fetch, repository import, or AI tool execution. Record allowed schemes, destinations, redirect rules, credentials, and whether response bodies are returned. Use a local replica with a tiny HTTP canary reachable from its worker. If the application runs in a container, localhost means that container; place the canary on an explicitly controlled test network instead of guessing host addresses.
The invariant is: every actual destination and credential transfer follows the feature's intended policy. A URL-fetching feature contacting your public receiver is often normal functionality. SSRF requires an unintended request or trust-boundary violation. SSRF mechanics.
Numbered test procedure
- Document the ordinary path. Submit a permitted static URL on your own receiver, containing a unique marker in its path. Record submission time, job ID, request method, receiver log, worker identity if available, and returned output. Confirm the browser did not fetch the URL directly by repeating through curl with the browser closed.
- Establish controls. Submit a second permitted URL that returns a different marker. Also submit a deliberately rejected scheme or destination from the feature's documented policy. Record whether rejection happens before queueing, at DNS resolution, after connection, or during response handling. A DNS lookup alone establishes resolution, not HTTP access.
- Test one policy boundary in the replica. Compare a permitted receiver with an owner-provided private canary that policy should block. If arbitrary public destinations are intended, concentrate on restricted network ranges, forbidden ports or schemes, or unintended credentials, using only designated canaries. Never use production metadata credentials as proof.
- Test redirect handling. Configure your first controlled endpoint to return one redirect to the second controlled canary. First verify both endpoints independently. Submit the first URL and correlate logs to show whether the second destination was validated. An initial allowlist check is insufficient if the actual redirected destination crosses the rule. Where authorized, study DNS validation/connection consistency in the replica with a controlled resolver; do not perform Internet-wide rebinding experiments. OWASP SSRF prevention.
- Inspect authority and output. Check whether a synthetic authorization header, test cookie, or privileged worker identity reaches the receiver unexpectedly. If the app displays a response, return only a fixed canary string. Distinguish blind connectivity from a readable internal response, and identify whether headers, method, body, or destination are attacker-controlled.
- Interpret and stop. Correlate exact marker, time, source, request, and job. Rule out your browser, webhook delivery by design, scanners, and unfurling services. Stop after the first controlled boundary violation; do not sweep adjacent hosts or ports. Shut down canaries and delete test jobs.
Command card
Run the receiver in a new empty directory in one terminal. This Python server serves only the canary directory and binds loopback. It is a local test receiver, not production hosting. Python HTTP server.
mkdir -p ssrf-canary
printf '%s\n' 'BB-SSRF-CANARY-01' > ssrf-canary/probe.txt
python3 -m http.server 8765 --bind 127.0.0.1 --directory ssrf-canary
In another terminal, send one request to your local application's observed fetch route. The example assumes same-host processes and JSON request data.
curl -q --noproxy '*' --max-time 10 --header @A.header \
-H 'Content-Type: application/json' \
--data '{"url":"http://127.0.0.1:8765/probe.txt?case=BB01"}' \
http://127.0.0.1:8000/api/preview
Report threshold and checklist
Report the prohibited destination or credential boundary actually reached. Label DNS-only evidence and environmental limitations explicitly. An open redirect or successful public image import alone does not prove internal access.
- Feature's destination contract written down.
- Receiver ownership and worker network namespace established.
- Ordinary fetch and rejected control recorded.
- Redirect destination and credential behavior inspected.
- No metadata secrets, unrelated internal addresses, or port sweeps used.
Local curl examples bypass environment proxies with --noproxy '*'; -q alone only suppresses curl configuration files. These snippets target loopback and do not follow redirects. curl manual.
Resolver, retry and connection identity
In source review, trace the URL parser, A/AAAA lookup, selected socket address, redirect handling and retry path as one decision. Verify the checked address is the one actually connected to; a second resolution, IPv6 path, alternate IP representation, redirect or worker retry must not escape the intended policy. For a DNS-change hypothesis, use a controlled resolver and designated canaries in the replica, record returned addresses and actual connections, and compare with a fixed-answer control. Merely accepting an unusual URL spelling does not prove a prohibited connection. Keep TLS verification enabled where HTTPS is used; do not change the client trust model to make the proof succeed. OWASP SSRF prevention.
Browser execution, cross-origin reads, and forged actionsUse real browser behavior to distinguish XSS, CSRF, CORS, and messaging failures from header-only leads.
Open canonical module and reverse references
- 01Locate source and browser sink
- 02Prepare two controlled origins
- 03Establish normal behavior
- 04Run browser-feasible probe
- 05Verify execution or state
- 06Remove stored content
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and invariant
Use two controlled accounts and separate browser profiles. For cross-origin tests, use a researcher page and the local target on different ports; these are different origins but may still be the same site. For conclusions involving SameSite cookies, reproduce with genuinely different sites in an authorized HTTPS setup. Record browser version, cookie attributes, CSP, iframe sandboxing, request initiator, and navigation context.
The invariant is: a lower-trust origin or stored value cannot execute in the application origin, read protected responses, or cause an unauthorized credentialed action. Each of those outcomes is a separate test. XSS, CSRF, CORS.
Numbered test procedure
- Map sources and sinks. Start with a distinctive text marker. Track it through reflected HTML, attributes, JavaScript strings, DOM insertion, Markdown preview, stored comments, embedded documents, and
postMessagehandlers. Record what is escaped and whether rendering occurs server-side or in JavaScript. Do not copy one payload across unrelated contexts. - Build a normal and inert control. As A, save plain text into a private object B is allowed to view. Verify B sees it. An HTML-escaped version is the inert comparison. For a local HTML text sink, a marker-only example is
<img src=x onerror="document.documentElement.dataset.bb='BB-XSS-01'">. Inspect B's DOM for the resulting attribute; the payload never reads cookies or sends data. If CSP blocks execution, preserve that outcome instead of claiming execution. - Test CSRF delivery. Capture B's normal reversible preference change. From the controlled other origin, reproduce it using a browser-deliverable form or fetch request without a manually inserted target bearer token. Verify which cookies were actually sent, whether preflight occurred, and whether the setting changed in a fresh target tab. Use an invalid CSRF token or logged-out profile as a control. A manually edited HTTP request is not by itself a browser exploit.
- Test CORS reads. From the researcher page, call a private test endpoint with
credentials: 'include'and attempt to read the response body. Compare an allowed application origin, the researcher origin, and an anonymous request. Save the actual browser result and credential behavior. A request reaching the server is different from JavaScript reading its response; wildcard origins do not enable credentialed response access. Browser CORS behavior. - Test messages and embedding where present. For an observed
postMessagehandler, send the expected message shape from your controlled page and change only the sender origin. Verify a harmless state marker or a restricted action on your account. For iframe interaction, prove the sensitive action is practically inducible, rather than reporting absent framing headers alone. - Interpret delivery and cleanup. Record which account planted content, which viewed it, the effective origin, and the boundary crossed. Remove stored payloads and reverse preference changes. A payload that only executes after the tester pastes JavaScript into DevTools has not established an application injection path.
Command card
Create browser-probe/index.html locally with this content, then serve it using the command below. Open it while the separate controlled target session exists; confirm that your browser's cookie policy permits the intended experiment.
<!doctype html><meta charset="utf-8"><pre id="result"></pre>
<script>
fetch('http://127.0.0.1:8000/api/me', {credentials: 'include'})
.then(r => r.text())
.then(t => { document.querySelector('#result').textContent = t; })
.catch(() => { document.querySelector('#result').textContent = 'Read blocked or request failed'; });
</script>
python3 -m http.server 8766 --bind 127.0.0.1 --directory browser-probe
Report threshold and checklist
Report cross-user execution, a browser-readable private response, or a forged protected state change with realistic delivery. Reflection, arbitrary Origin acceptance in curl, missing CSRF tokens, and same-site laboratory results generalized to cross-site attacks are common false positives.
- Real browser reproduces the claimed behavior.
- Origin and site relationships recorded separately.
- Credentials, CSP, preflight, and redirects observed.
- Benign marker demonstrates only the required impact.
- Stored content and account settings cleaned up.
Uploads, downloads, paths, archives, and XMLFollow a file through acceptance, storage, processing, publication, and retrieval while preserving ownership and path boundaries.
Open canonical module and reverse references
- 01Map file lifecycle
- 02Seed harmless canaries
- 03Change one file property
- 04Check parser and destination
- 05Verify another controlled reader
- 06Remove every derivative
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and invariant
Use a local replica, a tiny supported file, two controlled accounts, and a dedicated scratch directory. Identify upload handler, object store, converter, background worker, CDN/download route, and deletion path. Place a unique harmless canary just outside the permitted download root, through normal local filesystem access. The invariant is: user-controlled file metadata or contents cannot escape storage, interpretation, or ownership rules.
Separate filename, MIME declaration, actual bytes, storage key, filesystem path, and retrieval origin. Each component may normalize them differently. Acceptance of an unusual extension alone does not prove unsafe execution. Upload handling.
Numbered test procedure
- Record a supported baseline. Upload a tiny text/image fixture through the documented route. Save its object ID, resulting filename, declared and detected type, processor status, retrieval headers, and response bytes. Verify A can retrieve it and B cannot when the file is private.
- Change one property. Resubmit the same harmless content with a different filename or MIME declaration, independently. For a type-restricted image endpoint, text labeled
image/pngshould either fail or be safely handled. Inspect the stored result and actual browser rendering. Do not upload executable shells or server configuration files to demonstrate an extension mismatch. - Check path containment. In the local replica, request the planted out-of-root marker using one relevant
../path. Compare a valid in-root file and a nonexistent sibling path. Preserve the raw request target and server-resolved path; a client or proxy may normalize dot segments before they reach the application. If testing writes, use only a dedicated disposable output directory and check that an outside marker is not created. Path traversal. - Check ownership through derivatives. As B, request A's controlled original, thumbnail, conversion result, archive export, and any observed signed URL. Record whether the URL is intentionally a share capability and its documented lifetime. Deleting the primary record does not prove every derivative became inaccessible; evaluate the product's promised behavior.
- Check archive and XML processing locally. For an extractor, create a tiny archive with one ordinary member and one canary path outside its designated extraction directory; inspect resolved paths without overwriting anything important. For an XML parser, compare a plain literal field with one external entity referencing the controlled HTTP canary from the SSRF module. Correlate callback logs and returned marker. Do not use recursive entities, oversized documents, or operating-system files. XML acceptance or a DOCTYPE error alone is not external-entity resolution. XXE.
- Interpret downstream effects. Wait for the single test job to finish. Compare bytes, filesystem location, ownership, browser origin, and any parser callback. Source review should locate the exact join/extraction/parse operation and whether containment checks occur after normalization. Delete originals, derivatives, exports, and queued test jobs.
Command card
This creates a local download fixture. Configure the replica's allowed download directory as file-lab/public; illustrative endpoint and parameter names must match the application.
mkdir -p file-lab/public evidence/files
printf '%s\n' 'BB-INSIDE-01' > file-lab/public/inside.txt
printf '%s\n' 'BB-OUTSIDE-01' > file-lab/outside.txt
curl -q --noproxy '*' --max-time 10 --get --header @A.header \
--data-urlencode 'name=inside.txt' \
-o evidence/files/baseline.txt \
http://127.0.0.1:8000/api/download
curl -q --noproxy '*' --max-time 10 --get --header @A.header \
--data-urlencode 'name=../outside.txt' \
-o evidence/files/traversal.txt \
http://127.0.0.1:8000/api/download
Report threshold and checklist
Report a proven outside-root read/write, unauthorized controlled-file access, unsafe browser/server interpretation, or prohibited parser fetch. A reflected filename, harmless stored extension, public sharing by design, and asynchronous rejection are frequent false positives.
- Upload and retrieval rules documented independently.
- Only tiny, synthetic files and dedicated canaries used.
- Filename, MIME, contents, and ownership varied separately.
- Raw request and resolved destination retained.
- Processed derivatives included in evidence and cleanup.
Local curl examples bypass environment proxies with --noproxy '*'; -q alone only suppresses curl configuration files. These snippets target loopback and do not follow redirects. curl manual.
Containment beyond dot segments
For a link-capable filesystem or archive format, test a tiny symbolic-link fixture in the disposable directory as a separate case. Point it only to the already-planted harmless outside canary, then exercise the product's real reader/extractor. Record the final resolved destination and the identity that opened it. A lexical .. rejection does not establish containment when links, absolute member paths, platform separators, or case collisions change the destination. Do not generalize a local administrator-created link into a remote vulnerability: show how the stated attacker can create or influence that link through the product.
Pin the runtime and actual extraction API/filter. Python's tar extraction filters can reject outside destinations and links, but filter availability, explicit overrides, and additional resource limits matter. A failed extraction may also leave earlier members on disk; inspect and remove only the disposable output. If a TOCTOU claim depends on replacing a path between validation and opening, demonstrate that timing separately in the replica; a static link test does not prove a race. Python extraction filters and limitations.
- Final resolved file or link target stays inside the intended root.
- Actual runtime/filter and partial-extraction cleanup documented.
Workflow invariants, replay, idempotency, and racesFind security consequences in workflow ordering and concurrency, with serial controls and durable-state evidence.
Open canonical module and reverse references
- 01Write business invariant
- 02Capture normal transitions
- 03Change ordering or replay
- 04Compare serial control
- 05Try bounded lab concurrency
- 06Verify committed state
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and invariant
Choose one reversible workflow in a local or nonfinancial sandbox: accepting an invitation, creating one test resource, applying a test credit, approving a draft, or consuming a one-time action. Write the exact invariant, for example: one invitation grants at most one membership, to its intended recipient and role. Record preconditions, state transitions, actor permissions, durable result, retries, and cleanup. Logic findings depend on intended rules, not whether the request is technically valid. Business logic.
Numbered test procedure
- Draw the normal transition table. List draft → submitted → approved → completed, or the product's actual equivalent. For each edge, record actor, required prior state, immutable fields, and transaction identifier. Perform one normal run with synthetic objects and save before/after state, not just HTTP responses.
- Test skipped and reordered transitions. On a fresh disposable object, attempt the observed completion request before approval. Then change only a value that should be frozen after approval, such as recipient or test quantity. Compare with a fully approved control and an invalid object ID. If a transition is intentionally optional, adjust the invariant rather than treating missing UI steps as a bug.
- Test serial replay. Repeat the same completed operation once after it settles. For documented idempotency keys, compare the same key/same body with the same key/different body, using sandbox data. Determine whether keys are scoped by actor, tenant, endpoint, and expiry. Returning the first result again can be correct idempotent behavior.
- Test boundary values selectively. Use zero, a single negative value, or a precision boundary only where the sandbox permits it. Record server-calculated totals and immutable values. Avoid real billing, inventory, refunds, transfers, or customer notifications. A strange displayed number without a committed consequence is not enough.
- Test concurrency only in an authorized setting. First demonstrate expected serial behavior. Reset a test object, then send exactly two simultaneous requests as a small experiment. The local example below uses a barrier; it does not guarantee server arrival within the vulnerable interval. If there is no overlap, record inconclusive timing rather than absence of a race. More aggressive synchronization belongs in the isolated lab. Race conditions.
- Inspect after completion. Wait for both requests and relevant workers. Count durable memberships/resources or inspect the ledger of synthetic events. Two success responses may correspond to one commit. Repeat only to resolve a specific uncertainty, then restore the object and stop at the first demonstrated invariant failure.
Command card
This sends exactly two requests to a disposable local invitation endpoint. Put a valid local test bearer token in A.token, with restrictive permissions, and replace the route with the observed one. Create a fresh invitation for each serial/concurrent comparison. Run against localhost only.
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from threading import Barrier
from urllib.request import Request, build_opener, ProxyHandler, HTTPRedirectHandler
from urllib.error import HTTPError
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
gate = Barrier(2)
token = Path('A.token').read_text().strip()
def attempt(index):
req = Request('http://127.0.0.1:8000/api/invitations/TEST-INVITE/accept',
data=b'{}', method='POST',
headers={'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'})
gate.wait()
try:
with build_opener(ProxyHandler({}), NoRedirect()).open(req, timeout=10) as response:
return index, response.status, response.read(1024).decode(errors='replace')
except HTTPError as error:
return index, error.code, error.read(1024).decode(errors='replace')
with ThreadPoolExecutor(max_workers=2) as pool:
print(list(pool.map(attempt, range(2))))
Report threshold and checklist
Report a durable violation of the stated rule, with serial controls, initial conditions, minimal request sequence, and final state. Duplicate responses, transient UI counters, expected retries, and intentionally cumulative operations are common false positives. Describe the demonstrated consequence; do not infer financial loss from a sandbox counter alone.
- Business rule and expected actor written down.
- Normal and serial-replay controls completed.
- Fresh fixture used for each experiment.
- Only bounded authorized concurrency used.
- Durable state and asynchronous effects verified.
The Python example disables environment proxies and redirect following. A redirect is captured by the HTTPError branch; inspect its destination before any separate request.
Events that arrive after the request
Apply the same invariant to workers and integration events, not only the HTTP endpoint. Record a deduplication key's scope and when it becomes durable relative to the protected effect. A claim written before a worker fails can lose work; a claim written after the effect can permit duplicates. Use an instrumented fault point in the replica to distinguish these cases, and inspect committed state after retry. Then run the webhook event integrity procedure for signature, event ownership and out-of-order delivery checks. Do not assume provider delivery time is a reliable object version or that an HTTP acknowledgement proves processing completed.
REST, GraphQL, gRPC, WebSocket, and SSE permission parityCarry the same permission invariant across protocols, nested fields, streams, and alternate API versions.
Open canonical module and reverse references
- 01Inventory observed operations
- 02Choose one permission invariant
- 03Compare controlled identities
- 04Test alternate protocol path
- 05Check stream lifecycle
- 06Record semantic result
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and invariant
Start from normal client traffic and supplied API schemas. Create two controlled accounts and private synthetic objects. Map REST paths, GraphQL operations/fields, gRPC methods, and WebSocket message types to the same business action. The invariant is: changing transport, API version, batching, or connection lifetime does not widen an actor's permission. Reuse the authorization matrix and session modules; protocol-specific testing adds delivery details rather than duplicating those foundations.
Numbered test procedure
Build a semantic inventory. Record operation name, method/path, actor, tenant selector, object ID, request schema, response fields, and side effects. Observe one success and one forbidden controlled-object request per available protocol. Preserve response bodies, GraphQL errors, gRPC status/trailers, or socket messages; HTTP status alone does not define application success.
Check REST parity. Replay the same controlled-object rule through observed v1/v2 routes, nested resources, exports, and a two-item batch. Compare read, update, and server-controlled properties independently. An alternate route being live is not a vulnerability unless its behavior violates the intended permission or lifecycle rule.
Check GraphQL resolvers and fields. Begin with the client's existing query. As B, change only A's known object ID, then request one protected field omitted from B's normal response. Try the observed global-node route if present. Use at most two aliases as a bounded comparison of resolver behavior; do not use recursive depth or massive aliases. A schema or introspection response helps discovery but is not a confidentiality breach by itself. Where cookie authentication and alternate HTTP methods exist, check a reversible mutation through the observed transport: GraphQL GET is for queries, not mutations. If claiming CSRF, prove delivery in a real browser using the browser-trust procedure. Persisted query IDs are operation selectors, not permission proofs. GraphQL HTTP semantics. GraphQL testing.
Check gRPC context. Use the provided
.protofile or authorized reflection to identify one method. Compare channel identity, per-call credentials, tenant metadata, and the selected resource. Repeat a forbidden request with B's genuine credentials, not a token fabricated from decoded claims. For streaming methods, check whether a permitted start improperly authorizes later messages for another controlled resource. Channel credentials and call credentials serve different roles. If an HTTP/JSON transcoding gateway or gRPC-Web route is actually deployed, replay this same invariant there: a translated request can travel through different authentication middleware. Preserve the gateway path and backend method instead of inferring exposure from a.protoannotation. HTTP/JSON transcoding. gRPC authentication.Check WebSocket and SSE lifecycle. Capture a normal handshake and one subscription message. In B's profile, request A's private room using its known ID; have A send a single harmless marker. Remove B from a previously shared room and test one further message on the existing socket. For cross-site hijacking, use a real controlled origin and actual browser cookies. An accepted forged Origin header from a nonbrowser client does not prove browser delivery. OWASP WebSocket security. For server-sent events, use the observed event endpoint with A and B, then remove B and inspect one new synthetic event. A reconnect cursor or
Last-Event-IDselects delivery position; test that it cannot select another controlled tenant's history. Close the browser EventSource during cleanup because it normally reconnects automatically. SSE connection and reconnection.Interpret and consolidate. Read partial data as well as error arrays; an error on one field can coexist with leaked data elsewhere. Correlate socket subscription IDs and asynchronous messages. Group instances by shared root cause only when the evidence supports it, and name untested transports explicitly. Close sockets and remove test resources.
Command card
Prepare protected A.header and B.header files as described in the authorization module. The example assumes the local schema exposes document(id:). Replace it with the already observed query, not an invented production operation. The second request changes only identity.
mkdir -p evidence/protocols
cat > document-query.json <<'JSON'
{"query":"query($id:ID!){document(id:$id){id title}}","variables":{"id":"O1"}}
JSON
curl -q --noproxy '*' --max-time 10 --header @A.header \
-H 'Content-Type: application/json' --data @document-query.json \
-o evidence/protocols/a.json http://127.0.0.1:8000/graphql
curl -q --noproxy '*' --max-time 10 --header @B.header \
-H 'Content-Type: application/json' --data @document-query.json \
-o evidence/protocols/b.json http://127.0.0.1:8000/graphql
For a local socket, use the browser's Network → WS view to copy the observed message schema. Change one controlled room ID and send one message; protocol libraries often add framing, acknowledgements, or subscription IDs that a generic text frame would omit.
Report threshold and checklist
Report forbidden controlled data/action, a protected-field leak, or unauthorized ongoing delivery after the applicable revocation point. Reflection, introspection, websocket connectivity, and partial errors without forbidden output are discovery observations.
- Normal client operation and schema preserved.
- Same permission rule compared across available transports.
- Application-level errors and partial output inspected.
- Authentication at connection and message levels distinguished.
- Every stream closed and fixture cleaned up.
Proxy normalization, cache isolation, and request boundariesInvestigate layered HTTP behavior with isolated cache keys and controlled front-end/back-end traffic.
Open canonical module and reverse references
- 01Map HTTP processing layers
- 02Prove isolation
- 03Establish normal cache behavior
- 04Change one interpretation input
- 05Verify second controlled client
- 06Purge and document
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and invariant
Use a local proxy plus application, or a program-provided isolated cache namespace and upstream pool. Record protocol on each hop, request normalization, cache key, cache policy, authentication placement, and whether connections are reused. The invariant is: all layers agree on the request target, request boundary, and which client may receive a cached representation. These tests can affect requests other than your own; an unproven random query parameter is not isolation.
Numbered test procedure
- Map the layers. From configuration or operator-provided logs, identify browser → CDN/proxy → application. Capture an ordinary controlled URL from two independent clients. Record
Age, cache-status headers where present,Vary,Cache-Control, effective host/path, and origin logs. A header name or latency difference alone does not establish caching. - Prove the test namespace. Configure a dedicated local path or verified cache-key component. Request two distinct canary paths and confirm they remain separate through origin and cache logs. A query string may be ignored, normalized, or partially keyed. If separation cannot be established in a shared environment, keep mutation experiments in the replica.
- Test cache poisoning. On a fresh isolated key, vary one input that changes origin output but may not be in the cache key, such as an observed forwarding header used in a generated absolute link. Use a harmless marker host under your control. Make a second request without that input and inspect whether the marker persists. Confirm a control key remains clean. The evidence must show an altered representation served from shared cache to the second controlled client. Cache poisoning.
- Test cache deception. Create a private page containing A's unique synthetic marker. In the lab, compare the canonical path with one suffix/path-normalization variant that the origin resolves to the same page but the cache may treat as static. Request that variant as A, then as anonymous B. Verify B receives A's marker because of caching; compare B's direct origin response or a fresh key. Sharing via an intentionally public capability URL is not deception. Cache deception.
- Test parsing agreement in isolation. Capture the bytes seen by front end and backend for a normal request. In the lab, create one malformed framing case and observe whether both components reject it consistently. For ambiguous length/framing, verify how many requests each layer parsed and where the second controlled request begins. Do not treat a timeout as proof, or use unrelated users' responses to demonstrate desynchronization. HTTP/1.1 framing and HTTP/2-to-HTTP/1 translation must be assessed separately. HTTP/1.1 framing, Request smuggling.
- Verify consequence and clean up. Save origin/cache logs, exact requests, normalized targets, cache keys, and client identities. Purge only the dedicated test namespace or restart the disposable cache. Report whether the outcome is poisoned content, private response disclosure, authorization normalization bypass, or request-boundary disagreement; those are distinct findings.
Command card
These ordinary requests establish a local cache baseline. Configure /bb-cache/case-01 and /bb-cache/case-02 as separate keys in your own proxy first. A later poisoning probe must vary only a specific source-identified input.
mkdir -p evidence/cache
curl -q --noproxy '*' --max-time 10 -D evidence/cache/first.h \
-o evidence/cache/first.txt http://127.0.0.1:8080/bb-cache/case-01
curl -q --noproxy '*' --max-time 10 -D evidence/cache/second.h \
-o evidence/cache/second.txt http://127.0.0.1:8080/bb-cache/case-01
curl -q --noproxy '*' --max-time 10 -D evidence/cache/control.h \
-o evidence/cache/control.txt http://127.0.0.1:8080/bb-cache/case-02
Report threshold and checklist
Report a controlled second-client effect or demonstrated parser disagreement with a meaningful security boundary. Mere caching, unexpected 400/502 responses, reflected Host values, and differences caused by the testing proxy are common false positives.
- Cache namespace and upstream isolation verified.
- Every HTTP hop and protocol recorded.
- Normal, mutated, second-client, and clean-key controls saved.
- No production response-queue capture or broad desync probing.
- Test cache entries purged after evidence collection.
Local curl examples bypass environment proxies with --noproxy '*'; -q alone only suppresses curl configuration files. These snippets target loopback and do not follow redirects. curl manual.
Cache semantics and revocation control
Interpret response directives precisely: unqualified no-cache permits storage but requires validation before reuse; no-store prohibits storage/reuse subject to the specification's defined exceptions; unqualified private prevents shared-cache storage but permits private-cache storage. Set-Cookie alone does not disable caching. Requests containing Authorization have special shared-cache restrictions, with explicit exceptions such as public and s-maxage; cookie-authenticated requests need their own correct response policy and cache partitioning. Read the effective configuration and observe behavior. RFC 9111, sections 3.5, 5.2 and 7.3.
In an isolated namespace, compare A, anonymous B, and a fresh control key after A changes or deletes the private marker. Also exercise one normal conditional request if the application uses validators. Inspect which identity the origin checked and which stored body the cache reused after a 304. Do not infer privacy from an Age: 0, absent cache header, or an initial miss. Report stale post-revocation access against the product's promised lifecycle, distinguishing a previously downloaded copy from a newly served shared response.
- Storage, revalidation and private/shared-cache behavior distinguished.
- Fresh, cached and post-change responses compared where applicable.
OSS lab: pin, install, prove, resetTurn an eligible release into a repeatable synthetic environment with preserved versions, realistic controls, and an explicit reset recipe.
Open canonical module and reverse references
- 01Qualify release
- 02Freeze inputs
- 03Review installation
- 04Build fixtures
- 05Compare controls
- 06Reset and repeat
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Start with a reproducibility contract#
Record repository, product edition, supported release, immutable commit, package locks, image digests, operating system, database, authentication mode, and enabled extensions. Keep tested, inferred, and unknown versions separate. A source tag does not identify the image running in a vendor deployment. Your initial output is a manifest plus a working baseline, before any vulnerability attempt.
Use a disposable VM for an unfamiliar project's build. Review its Dockerfiles, build backends, package lifecycle scripts, Makefiles, and downloaded installers first. Provide synthetic credentials and fixtures. Separate dependency preparation from validation; disable unnecessary external integration calls during validation. Preserve the installation instructions you followed and every deviation. This makes it possible to distinguish a supported insecure default from a flaw introduced by the research setup.
Freeze source without overwriting work#
These are adaptable local examples, not a claimed execution. Start with an existing reviewed clone at ./repo; create a fresh output directory. Substitute a real eligible release in the second command. A detached worktree allows a separate copy at that revision while sharing Git objects. Git worktrees.
mkdir -p ./evidence
git -C ./repo rev-parse --verify 'v1.2.3^{commit}'
git -C ./repo worktree add --detach ../lab-source v1.2.3
git -C ./lab-source rev-parse HEAD > ./evidence/source-commit.txt
git -C ./lab-source status --porcelain=v1 > ./evidence/source-status.txt
git -C ./lab-source submodule status --recursive > ./evidence/submodules.txt
Expected: a full commit identifier and an initially clean status file. A submodule entry is inventory, not confirmation its source has been obtained or reviewed. Save compiler/runtime/tool versions separately. Never silently replace a failing eligible release with current main.
Review the effective deployment#
Compose configuration can interpolate environment values, so use a synthetic lab environment and inspect output before sharing. Render first; check host mounts, image references, ports, capabilities, external volumes, privileged mode, and startup scripts. A project name alone does not isolate explicitly named shared volumes. Docker Compose config.
docker compose -p bounty-lab -f ./lab-source/compose.yaml config \
> ./evidence/compose-effective.yaml
docker compose -p bounty-lab -f ./lab-source/compose.yaml \
up -d --wait --wait-timeout 120
Run up only after review and required dependency preparation. --wait depends on declared health checks for application readiness; a running process alone is insufficient. Establish login, ordinary creation, retrieval, and a worker task through the real interface. Docker Compose up.
| Fixture | Normal operation | Boundary case |
|---|---|---|
| Tenant A and B | Each reads its own document | A requests B's synthetic document |
| Member and administrator | Administrator manages invitations | Member attempts the same operation |
| Import directory | Valid sample stays inside destination | Malformed sample attempts a harmless boundary crossing |
| Background worker | Queued operation completes once | Membership changes before job execution |
Preserve the failing and passing stories#
Example trace: member A requests an export, the route returns a job ID, and the worker reads a document owned by B. Capture the request, job record, worker resolution, synthetic output, and authorized B control. A direct call to the export helper explains the implementation but does not establish route reachability. Continue in source data flow and reuse the authorization matrix.
Diagnose setup failures before testing: a migration error invalidates database assumptions; disabled workers invalidate asynchronous conclusions; missing enterprise features limit edition claims. Record unresolved differences instead of compensating by disabling security controls.
Reset and report gate#
Prefer restoring a dedicated VM snapshot or documented fixture/database snapshot. Include queues, object storage, cache, local mail, and generated files in the reset recipe. Container restart is not state reset. Repeat the minimal reproduction after restoration; a result dependent on an earlier experiment is not yet isolated.
- Eligible revision, dependency locks, and deployment differences recorded.
- Normal user workflow and comparison account work.
- Required worker/integration fixtures are present.
- Synthetic evidence survives outside reset storage.
- Clean restoration reproduces the same boundary failure.
- Report distinguishes product behavior from lab-only configuration.
Source review: entry point to protected operationTrace attacker control and authorization through handlers, services, queues, and sensitive operations; promote only reachable violations of a stated invariant.
Open canonical module and reverse references
- 01State invariant
- 02Find entry
- 03Trace transformations
- 04Check guards
- 05Exercise interface
- 06Retest sibling paths
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Write the invariant first#
Choose a business rule precise enough to falsify: “a member can export documents only from a project where membership is currently active.” Record actor, object, operation, tenant, and when authorization must hold. A suspicious function becomes useful when you identify who can reach it and what protected state it changes. Reuse the authorization matrix for principal/object combinations and injection tracing for interpreter-specific input handling.
Start forward at a real interface: HTTP route, websocket event, CLI argument, uploaded project, plugin callback, or queue producer. Then work backward from the sensitive read, write, process invocation, parser, or credential use. Join the two traces; unresolved dynamic dispatch is an explicit gap. Check route registration and feature gating rather than assuming a named handler is exposed.
A worked asynchronous trace#
The following is a hypothetical review worksheet, not a finding against a named project.
| Hop | Value or context | Question to resolve |
|---|---|---|
POST /exports |
Authenticated member A supplies document_id |
Does the route check document membership or only login? |
ExportService.create |
A job stores document_id, requested_by |
Is the trusted tenant copied from the session or accepted from input? |
| Queue serialization | Job survives after request completes | Can another low-trust producer alter the fields? |
run_export(job) |
Worker reloads document by ID | Is current membership enforced at execution time? |
| Object storage write | Export receives a download reference | Who can fetch it, and can authorization expire? |
A useful candidate is “worker rehydrates an object globally after a tenant-scoped request.” Confirm whether the earlier guard binds the same immutable identifier, whether membership revocation should affect queued jobs, and whether the output is actually accessible. Business requirements may deliberately snapshot permissions; document that distinction rather than treating every delayed check as defective.
Local search and modeling cheat sheet#
Examples inspect a reviewed local repository. Patterns are leads, not a complete inventory. Place evidence outside the scan root; prepare local rules before offline/private-code analysis.
rg -n --hidden -g '!.git' \
'route\(|router\.|websocket|add_task|enqueue|\.delay\(' ./repo
rg -n --hidden -g '!.git' \
'pickle\.loads|yaml\.load|subprocess\.|child_process|exec\(' ./repo
semgrep scan --oss-only --config ./rules --metrics=off \
--disable-version-check --json \
--output ./evidence/source-leads.json ./repo
The Semgrep command uses reviewed local rules and the OSS engine. Inspect stderr, skipped files, and scan completion; an empty result with parser failures is not a clean audit. Rule licenses and engine capabilities are separate concerns. Semgrep CLI reference.
Model sources, propagation, context-specific sanitizers, and sinks. A path validator does not sanitize a shell argument; a role check does not validate serialized data. Custom wrappers can hide a source or sink from default rules. Create one vulnerable fixture and one safe sibling to validate your model. Static taint analysis does not automatically establish tenant policy or full runtime reachability. Semgrep taint model.
Prove the boundary through supported use#
Instrument the local application to log identifiers, decisions, and function names without credentials. Reproduce from the ordinary interface with synthetic accounts. Keep four observations together: allowed own-object request; denied or expected-to-deny foreign-object request; actual protected effect; resulting output access. Repeat without instrumentation changing security behavior. If only an internal helper is callable, qualify the finding as a library/caller-contract issue until a consumer path is established.
Inspect alternate callers after proving one flaw: bulk API, export service, websocket subscription, scheduled task, and administrative UI may reuse the same operation with different guards. Group equivalent paths by root cause and record tested variants. Do not inflate one shared defect into five independent findings.
Completion and report threshold#
- Invariant identifies actor, object, operation, and trust boundary.
- Attacker-controlled field is tracked through transformations and aliases.
- Every guard is evaluated with concrete fixture values.
- Feature, edition, role, and configuration prerequisites are explicit.
- Real interface reproduces the protected effect beside an authorized control.
- Safe sibling and patch retest preserve legitimate behavior.
Submit the smallest connected argument: entry point → attacker control → ineffective boundary → sensitive operation → demonstrated impact. A sink search, scanner trace, or direct helper call alone belongs in the hypothesis queue.
Dependencies: from advisory match to reachable impactSeparate composition, advisory matching, runtime reachability, and exploit prerequisites so dependency reports describe product impact.
Open canonical module and reverse references
- 01Inventory layers
- 02Resolve advisory
- 03Trace consumer
- 04Check prerequisites
- 05Compare fixed behavior
- 06Bound affected versions
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Keep four questions separate#
An SBOM answers what a tool identified in an artifact. An advisory matcher asks whether a component version matches known vulnerability data. Reachability asks whether the relevant implementation is used. Exploitability asks whether an attacker can satisfy the conditions and cross a security boundary. Preserve each conclusion independently; a negative result at one layer does not answer every other layer.
Inventory source locks, installed runtime, development/build dependencies, and final release output separately. A vulnerable parser in a lockfile may be absent from the production image. A development-only package may still run inside a privileged publisher. Record ecosystem, package identity, resolved version, origin, dependency path, optional features, and artifact digest. Use CI and release analysis for build impact and source data flow for runtime impact.
Inventory and match locally#
Examples require installed tools, reviewed inherited configuration, existing local paths, and prepared OSV ecosystem databases. They are command templates, not claimed results. ./release-root means an already extracted, reviewed release tree.
mkdir -p ./evidence
syft dir:./repo -o cyclonedx-json=./evidence/source.cdx.json
syft dir:./release-root -o cyclonedx-json=./evidence/release.cdx.json
osv-scanner scan source --offline -r --format json \
./repo > ./evidence/osv-source.json
Syft supports directory inventory and structured SBOM output; detected composition depends on catalogers and available metadata. Compare the two inventories rather than assuming they represent the same execution environment. Syft documentation.
OSV source scanning uses supported manifests and lockfiles. Full --offline requires cached databases and avoids network activity; --offline-vulnerabilities has a narrower meaning and can permit other network features. Missing caches are an incomplete scan. Preserve database age and scanner stderr alongside findings. OSV source scanning, offline behavior.
Build a reachability worksheet for each serious candidate#
| Question | Concrete evidence | If unresolved |
|---|---|---|
| Is the resolved component affected? | Exact advisory range, vendored source, backport patch | Version match only |
| Is affected code present? | Release tree, build flags, linked module | Source-only candidate |
| Is the vulnerable API called? | Consumer call site and dynamic trace | Reachability unknown |
| Can an attacker control its argument? | Ordinary route/file/CLI input and transformations | No attacker path established |
| Are prerequisites satisfied? | Parser mode, operating system, feature, privilege | Conditional exposure |
| What changes across the boundary? | Synthetic output or bounded local fault | Impact unproven |
Hypothetical example: a document service ships an affected archive library, but the advisory concerns symbolic-link extraction. Trace upload acceptance → import job → actual extraction API → filter configuration → destination. A safe filter can defeat that specific path even when the version matcher is correct. Conversely, an optional compatibility flag may restore the unsafe behavior. Test supported configurations explicitly and reuse the pinned lab.
Check package origin as well as version#
For dependency confusion, first inspect registry selection, namespace ownership, fallback, lockfiles, integrity hashes, and install hooks. pip documents that candidates from configured locations are considered together, rather than treating an extra index as a guaranteed lower-priority fallback. pip install index behavior.
Reproduce with two local registries and synthetic names that belong to your lab. Use packages containing metadata and harmless marker files; record which origin wins under the target's resolver configuration. Do not publish a real organization's missing package name. Resolver selection is one proof step; a product-impact claim must also show an affected build/consumer and applicable package controls.
Control, report, and completion#
Run the same smallest input against the affected and patched dependency in equivalent consumers. If swapping versions also changes application behavior or configuration, isolate that difference before crediting the patch. Test normal input to ensure the “fix” is not simply a broken integration. Avoid claiming every historical release is affected from one sampled version.
- Source, runtime, build, and release inventories are labeled.
- Advisory and patch are checked for the exact resolved component.
- Backports, vendoring, and optional features are considered.
- Attacker input reaches the affected API through supported use.
- Local impact and a fixed/control case are preserved.
- Tested versions, inferred exposure, and unknowns are separated.
The reporting threshold is a reproducible vulnerable use or the program's explicitly accepted dependency criterion. An old version number alone should remain a triage lead.
Object storage, signed URLs and delivery boundariesFollow one private canary through upload, signing, CDN delivery and expiry without enumerating real objects.
Open canonical module and reverse references
- 01Create private canaries
- 02Map origin and delivery
- 03Compare anonymous and tenant access
- 04Test capability scope
- 05Verify cleanup and expiry
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and invariant#
Create two tiny text objects in controlled storage, one private object per tenant, containing different random markers. Record bucket/account, exact keys, region, encryption requirements, application ownership and delivery host. Obtain the intended read, write, share and revocation rules. Only an entitled caller may mint or exercise the capability promised by the product. Reuse authorization testing for tenant changes; this module adds cloud storage and delivery behavior.
Procedure#
Separate the access paths. Draw upload API → signer → storage object → CDN → browser. Add direct origin access, access points and background exports if the product uses them. Decide whether a file is meant to be public before calling anonymous retrieval a finding.
Establish object-specific baselines. The owner must retrieve the exact marker; an unauthenticated client and the other tenant should fail where privacy is promised. Inspect owner-provided policy and effective public-access controls. A permissive bucket statement can be overridden by account or other public-access blocks; a block-status response does not examine every application sharing path. S3 public-access controls
Inspect signed capability issuance. Ask tenant A to create its normal download or upload capability. Then change only the owned fixture's target key to B's key through the application's supported request. Compare authorization before signing and before asynchronous result release. Properly issued presigned URLs are intentionally usable by their holders; that property alone is not an authorization bug. S3 presigned URLs
Test the promised restrictions. Compare the allowed method/object with a changed method or the other synthetic key. Check the agreed expiry using timestamps and a single later request, allowing for documented credential lifetime and clock behavior. For write tests, allocate a separate disposable key and explicit permission; never overwrite an existing customer file. Capture the owner-confirmed object version and marker before and after.
Compare delivery and origin. Request the private fixture through the intended authenticated CDN path, then directly from its owned origin without credentials. The direct path should enforce the documented protection. A publicly intended origin is a valid control, not a bypass. CloudFront origin access control applies differently to normal S3 origins and website endpoints; match the actual configuration. CloudFront origin restrictions
Command card#
The first command reads at most 64 bytes of the exact fixture through unsigned S3 access. It performs no listing. Credentials, URLs and output files should stay private.
aws s3api get-object --bucket OWNED_BUCKET \
--key bounty/private-canary.txt --range bytes=0-63 \
--expected-bucket-owner YOUR_ACCOUNT_ID --no-sign-request \
--no-cli-pager --cli-connect-timeout 5 --cli-read-timeout 10 \
./anonymous-canary.bin
aws --profile lab-owner s3 presign \
s3://OWNED_BUCKET/bounty/private-canary.txt \
--expires-in 60 --region YOUR_REGION \
--cli-connect-timeout 5 --cli-read-timeout 10
curl -q --silent --show-error --max-time 10 --max-filesize 1024 \
--dump-header ./delivery.headers \
--output ./delivery-canary.txt \
https://OWNED_DELIVERY_HOST/bounty/private-canary.txt
Use the second command only to establish an intentionally issued positive control. Do not paste its bearer URL into reports or shared shell history. The curl command follows no redirects: inspect any redirect destination before testing it. Expected evidence is the marker or an access denial, not merely HTTP 200. S3 GetObject CLI, presign CLI
Interpretation and completion#
Report the precise failed boundary: anonymous private read, unauthorized capability issuance, cross-tenant overwrite, or direct-origin bypass. A 403 cannot distinguish every encryption, ownership and permission cause; an HTML login page is not object exposure. Avoid speculative findings based only on predictable keys or a reusable valid share URL. For URL-driven storage imports, reuse SSRF boundaries.
- Object owner, privacy promise and marker recorded.
- Anonymous, entitled and other-tenant cases compared.
- Signature lifetime distinguished from application-session lifetime.
- Origin and delivery tested against the same fixture.
- Fixture versions removed; temporary shares revoked where supported.
- Cached copies handled according to the owner's cleanup procedure.
For Azure SAS types and Entra versus Shared Key access, or GCP object ACLs and uniform bucket-level access, use Azure and GCP boundaries alongside this shared canary procedure.
RAG isolation: ingestion, retrieval, citations and revocationTrace document authorization through vector search, generated answers, citation downloads, caches and sharing changes.
Open canonical module and reverse references
- 01Seed private canaries
- 02Verify direct access
- 03Trace retrieval
- 04Change ownership state
- 05Compare exposed results
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and security invariant#
Use owned tenants A/B with private and shared collections. Place unique markers only in each tenant's private document. Record ingestion state, collection/document IDs and permissions. The invariant is: the effective caller can retrieve or resolve only currently authorized content through every supported path. Reuse the authorization matrix for object swaps and identity/session checks for tenant identity.
Unauthorized chunks may reach the model without appearing in its answer; filtered search can still return an unprotected citation download. OWASP identifies risks from shared vector stores and inadequate access controls. Test those boundaries separately. OWASP Vector and Embedding Weaknesses
Controlled procedure#
- Prove readiness. As A retrieve A's canary; as B retrieve B's. Then request B's document directly as A and verify denial. Record whether permissions belong to tenants, teams, individual users or externally synchronized groups. A canary missing from its owner's search is an ingestion problem, not isolation evidence.
- Compare retrieval paths. As A, issue the same semantic query used by B, with no secret marker in the query. Exercise search, chat, document lookup and citation download independently. In a local white-box fixture, log the authenticated principal, effective query filter, retrieved chunk IDs and resolver decision. Repeat with an absent tenant filter and a B collection ID only where the API actually accepts those fields.
- Inspect ingestion ownership. Upload a harmless A document while supplying B's collection or source reference. Confirm where the completed document is stored and who can read it. A rejected upload is a useful negative control; a successful job response alone does not show cross-tenant insertion. Follow async workers and document reindexing through source dataflow.
- Test sharing transitions. Share one synthetic document, retrieve it legitimately, then revoke access. After the product's documented synchronization interval, test a fresh conversation, an existing conversation, direct download, citations and cached answers. Record which content was already delivered before revocation; users retaining their earlier copy is not a new disclosure.
- Separate caches from filters. Warm a cache as B, repeat the same query as A, then repeat after an allowed lab cache clear or with a new synthetic document. Compare cold/warm results and cache keys. If only the warm path leaks, preserve that sequence. Test an A-only cache warm as the benign comparison.
Reusable case matrix#
| Case | Caller and state | Expected observation |
|---|---|---|
| Owner baseline | B → B-private document | Retrieval and resolver succeed |
| Tenant isolation | A → B-private document | No unauthorized chunk or download |
| Shared baseline | A → explicitly shared document | Allowed content is available |
| Revocation | A after share removal and sync | New protected retrieval is denied |
| Cache separation | B warms; A repeats | B's private result is absent |
Local command cheat sheet#
python3 -I -S - <<'PY'
import secrets
for owner in ('A', 'B'):
print(owner + '_PRIVATE_' + secrets.token_hex(16))
PY
rg -n 'tenant_id|collection_id|document_id|cache_key|namespace' ./src
python3 -I -S -m json.tool lab/retrieval-case.json
Store each generated marker only in its private fixture. Record caller, path, expected/actual document IDs, visibility and timestamp. These commands do not call hosted models or vector databases. Python secrets
Evidence and checklist#
Report the strongest observed boundary precisely: unauthorized chunk retrieval in logs, user-visible disclosure, or unprotected download. Do not present an internal trace as proof that an external attacker can see its contents. Exclude intentional shared collections, stale permissions within the stated synchronization window and markers included in the user's own query.
- Both owners can retrieve their own seeded documents.
- Search, answers and citation resolution were compared separately.
- Cold/warm cache and sharing-state controls are recorded.
- Actual caller identity and document ownership support the claim.
- Synthetic documents and temporary sharing grants were removed.
Beyond document retrieval#
Run AI state lifecycle when a private chunk is copied into a conversation, checkpoint, durable memory, background job or protocol cache. Record the new object owner and retention rule at each copy. A fresh search filter cannot repair an earlier copy served through an unprotected history/export API. Reuse the revocation controls above and distinguish content already delivered under a valid grant from a newly exposed private artifact.
MFA and passkeys: enrollment, proof, replacement, and recoveryFollow each authenticator from account binding to removal, and validate WebAuthn assertions without mistaking client UI or counter behavior for a security guarantee.
Open canonical module and reverse references
- 01Record assurance policy
- 02Enroll controlled factors
- 03Compare fresh and stale proof
- 04Check assertion bindings
- 05Replace and recover
- 06Verify removal
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and invariant
Reuse identity/session fixtures. Prepare two disposable accounts, recovery access, separate browser profiles, and one reversible protected operation. Record whether the product promises MFA, passwordless login, recent reauthentication, or a particular authenticator class; these are different contracts. The invariant is: a proof must authorize the intended account, ceremony, and assurance level, including factor management. Capture the server's stored factor list and audit events, not just a green browser badge.
Numbered procedure
- Test enrollment before authentication. Register A's first and second factors through the legitimate UI. Compare an authenticated registration with anonymous, MFA-pending, and stale-session attempts to complete that same observed operation. For an account-bound enrollment flow, begin in A and attempt completion in B, changing only the account/session context. Confirm the final factor owner with a fresh login. For username-less authentication, successful login to the factor's rightful owner is expected; it must not be reported as cross-account bypass.
- Exercise replacement and removal. Capture normal add, rename, delete, recovery-code regeneration, and fallback changes. In the replica, replay one sensitive factor change without the required recent proof, then with a fresh valid proof. Verify whether a newly added factor can log in and whether the removed factor still authenticates. Preserve a recovery route throughout. A factor addition that only changes a label proves nothing. Factor replacement deserves the same care as initial login. OWASP MFA management.
- Test OTP and recovery consumption separately. Use A's successful OTP once more in a fresh pending login during its validity window; use an invalid code as the negative control. Test one recovery code, then attempt the consumed code again. Regenerate the recovery-code set and check one old code. Do not brute-force codes or trigger real push/SMS floods. Record which transaction succeeded, not merely whether the numeric value appeared twice. TOTP verification must prevent reuse after successful validation; an identical code displayed during one time step is normal. RFC 6238. Saved recovery credentials also need explicit lifecycle controls. NIST authenticator management.
- Build a real passkey baseline. In the local application, open DevTools → More tools → WebAuthn, enable the virtual authenticator, select CTAP2 and the required resident-key/user-verification capabilities, then register through the application's normal page. Record the RP ID and credential owner. This tests server integration; it does not demonstrate compromise of a hardware authenticator. Chrome virtual-authenticator workflow.
- Separate signature checks from semantic checks. Compare a fresh assertion with a consumed assertion and one with an invalid signature. For username-first login, verify the credential belongs to the intended account; for discoverable credentials, verify the returned user handle resolves consistently to the credential owner. Trace the verifier's challenge, ceremony type, origin, RP-ID hash, signature, and presence/verification decisions. A local authenticator fixture can sign a deliberately mismatched case; changing signed JSON without re-signing only tests signature rejection. WebAuthn assertion fields.
- Check policy-sensitive WebAuthn branches. Verify UV when the ceremony requires user verification; a touch proving user presence does not by itself prove UV. Account for an explicitly configured related-origin deployment and applicable legacy AppID handling before calling an origin/RP mismatch a bypass. Counter anomalies are risk signals, not standalone clone proof; some authenticators keep zero counters. Synced credentials and absent attestation are not automatically weaknesses. Evaluate the deployed library and RP policy against the pinned WebAuthn Level 3 Recommendation.
Local inspection card
Export an owned test assertion in the application's JSON transport format to assertion.json. This prints selected metadata and a challenge fingerprint; it does not verify the signature or establish a vulnerability. If your wrapper uses different keys, adapt the field path.
python3 - <<'PYCODE'
import base64, hashlib, json
from pathlib import Path
x = json.loads(Path('assertion.json').read_text())
s = x['response']['clientDataJSON']
c = json.loads(base64.urlsafe_b64decode(s + '=' * (-len(s) % 4)))
print(json.dumps({
'type': c.get('type'), 'origin': c.get('origin'),
'crossOrigin': c.get('crossOrigin'),
'challenge_sha256': hashlib.sha256(c['challenge'].encode()).hexdigest()
}, indent=2))
PYCODE
Evidence and completion
Report a newly usable unauthorized factor, authentication below a required assurance level, or acceptance of a proof for the wrong account/transaction. Attach the expected rule, before/after factors, request order, independent fresh-login result, and cleanup. Missing notification alone is hardening evidence; a successful server rejection is a passed check.
- Enrollment and replacement evaluated independently.
- OTP, backup codes, and passkey ceremonies have separate controls.
- Username-first and discoverable flows distinguished.
- Signature rejection separated from signed-context validation.
- Removed test factors and credentials no longer authenticate.
- Virtual authenticator exports and live recovery secrets excluded from reports.
OAuth, OIDC, and SAML: transaction binding and token trustSeparate browser transaction binding, issuer trust, token purpose, and identity linking using controlled federation fixtures.
Open canonical module and reverse references
- 01Map issuers and clients
- 02Capture authorized flow
- 03Alter one binding
- 04Verify token semantics
- 05Check replay and linking
- 06Remove test grants
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and trust record
Use two controlled accounts, two registered lab clients when available, and a test identity provider. Record authorization server, client ID/type, exact redirect URI, grant, scopes, resource/audience, browser transaction, issuer/subject, and resulting local account. Pin deployed protocol profiles and libraries. Reuse the session lifecycle and authorization matrix; this procedure adds federation-specific decisions.
The invariant is: a valid credential for one transaction, client, issuer, or purpose must not silently authorize another. A decoded token, valid cryptographic signature, and successful login are three different observations.
Numbered procedure
- Establish browser binding. Capture a normal authorization-code login and a separate account-link operation. In the replica, deliver A's response into B's pending transaction and compare the resulting local account/link with untouched controls. Track which session initiated each transaction. Missing
statealone is not decisive when transaction-specific PKCE or another valid mechanism provides the required binding. Validate a registered redirect with exact matching, accounting for the native loopback-port exception. RFC 9700. - Check code and PKCE handling independently. On separate fresh authorization codes, compare the correct verifier, a wrong verifier, and a missing verifier. Then replay one already redeemed code. Where the flow supports PKCE, inspect whether removing the challenge can create a downgrade path. Keep client authentication, client ID, and redirect constant so rejection can be attributed correctly. A verifier is secret; a public challenge is not. Generate a new verifier for each real transaction. PKCE specification.
- Validate OIDC context. With the lab issuer, produce otherwise valid fixtures with one wrong issuer, client audience, expired time, or mismatched nonce when that flow requires it. Check profile-specific
azp, hash, and authentication-age rules rather than assuming every claim is universal. Local identity should use the issuer/subject pair; equal email claims do not establish that two identities may be merged. Compare UserInfo subject with the ID-token subject when UserInfo is used. OpenID Connect Core. - Test JWT trust and purpose. First show rejection of one invalid signature. Then use validly signed local fixtures for another resource or purpose: an ID token at an access-token endpoint, or an email-verification token at a session endpoint, where the implementation exposes that path. In source review, inspect algorithm allowlists, issuer-selected key material, key-ID lookup, and mutually exclusive validation rules. Do not turn a token-supplied key URL into a production callback experiment. A signature-valid but unauthorized token is a semantic trust failure, not necessarily broken cryptography. RFC 8725.
- Check refresh replay under the actual design. Record original and rotated refresh credentials separately. After one normal rotation, attempt the previous credential once from another local client and inspect both the returned token and a harmless resource read. If tokens are sender-constrained, test the relevant missing/wrong sender proof instead. Public-client refresh tokens require rotation or sender constraint; documented retry grace affects interpretation but does not erase demonstrated independent unauthorized access. OAuth refresh-token protection.
- Follow SAML from verified bytes to identity. In a local IdP/SP pair, compare a normal response with independently changed signed content, a wrongly targeted assertion, an expired assertion, and one replay. Trace the exact signed element from which the SP consumes identity and attributes; verifying an unrelated signed element is insufficient. For SP-initiated SSO check request correlation. For supported IdP-initiated SSO, missing request correlation is expected, but replay and recipient/trust checks still matter. Keep RelayState handling separate from assertion integrity. OWASP SAML processing.
Local command card
Prepare expected.header and other-resource.header using valid tokens minted by your lab issuer; each contains its complete Authorization: Bearer … line. The second token is intentionally valid only for a different resource under the issuer policy. Preserve the same subject and request where possible. The protected route is illustrative. These requests do not test a public provider.
mkdir -p evidence/federation
chmod 600 expected.header other-resource.header
curl -q --noproxy '*' --max-time 10 --header @expected.header \
-D evidence/federation/expected.h -o evidence/federation/expected.json \
http://127.0.0.1:8000/api/me
curl -q --noproxy '*' --max-time 10 --header @other-resource.header \
-D evidence/federation/wrong-resource.h -o evidence/federation/wrong-resource.json \
http://127.0.0.1:8000/api/me
A 401/403 is a useful control, but also inspect bodies and resulting identity. Successful use of the expected token establishes only the baseline. Token transport and audience selection must match the actual local deployment.
Evidence and checklist
Capture one invalid condition, accepted identity or capability, minimal synthetic response, and the intended trust configuration. Document any test signing keys you supplied: possession of the real IdP signing key already grants authority and is not a new bypass. A rejected code, intentional multi-audience policy, or unconsumed altered claim is not takeover evidence.
- Browser binding and token validity tested separately.
- Correct, wrong, omitted, and consumed proof cases kept distinct.
- Token purpose and issuer/audience validation separated from signature validation.
- SAML consumed identity traced to the validated signed element.
- Grants, sessions, test keys, and transient credentials removed after evidence capture.
Request values: duplicate parameters, parser conflicts and downstream queriesTrace the value a security layer validates through to the value the application or downstream service actually consumes.
Open canonical module and reverse references
- 01Map parser chain
- 02Freeze two object controls
- 03Vary one representation
- 04Trace authorized value
- 05Inspect consumed value
- 06Prove controlled impact
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and invariant
Map the actual processing chain: gateway, framework parser, schema validator, authorization code, service client, and worker. Use two synthetic objects, A-readable O1 and B-only O2, in a local replica with per-layer logs. The invariant is: the value authorized is the same value the protected operation consumes. This procedure covers application values; HTTP message framing stays in the proxy/cache procedure.
Numbered test procedure
- Create a value ledger. Capture raw request bytes, content type, parsed field type, validated value, authorized object and final downstream request. Start with O1 allowed and O2 denied under A's identity. Disable response caching for this local comparison. List which locations are accepted for each field: path, query, JSON, form, or multipart. Do not infer framework-wide behavior from a single handler.
- Compare duplicate parameters. Submit the original scalar, the alternative scalar, then the two values together in each order. Follow the value through validation and consumption. First-value, last-value, list or rejection behavior is not a vulnerability alone. A finding needs a security decision on O1 followed by an operation on O2, or another demonstrated rule violation. Test one observed parameter rather than fuzzing every field. WSTG parameter pollution.
- Preserve duplicate JSON names. Construct raw JSON bytes instead of a language dictionary, which may discard duplicates before sending. Compare a valid string, a repeated name, and a schema-invalid type such as a two-element array when a scalar is required. Record whether the gateway validates the first occurrence while the application consumes the last. RFC 8259 recommends unique object names and describes divergent receiver behavior; it does not make every accepted duplicate a vulnerability. Avoid asserting that all compliant parsers must reject them. RFC 8259, section 4.
- Check location and normalization precedence. Where the real endpoint accepts more than one representation, compare a query field against the same JSON/form field and a path ID against a body ID. In separate trials, inspect one relevant percent-decoding or Unicode-normalization difference. Only send supported content types; prove which route and middleware ran. A different error response or equivalent spelling alone is not an authorization bypass.
- Inspect server-created requests. A single external scalar may be interpolated into an internal query string or path. In a local service stub, use a marker such as
BB01&bb_probe=BB02as the complete outer value and inspect whether it remains one downstream value or becomes an added parameter. Encode it for the outer request exactly once. If a delimiter appears downstream, use only source-confirmed synthetic fields to establish a changed policy decision; do not guess internal administrative operations. Contrast correct URL-builder encoding with string concatenation. Server-side parameter pollution. - Confirm the boundary. Replay the minimal case through the normal ingress path and compare the strict-schema/consistent-parser negative control. Save the values each layer used and A's resulting access to the controlled O2 marker. Separate a reachable production path from a test harness configured with deliberately contradictory parsers. Remove both fixtures.
Command card: duplicate-preserving local requests
Assume an observed local /api/object-preview endpoint accepts scalar object, with test identities in A.header. Configure O1 as A-readable and O2 as B-only. The route names are placeholders, not discovered endpoints. This makes four ordinary, sequential requests without redirect following or inherited proxies.
mkdir -p evidence/parameters
curl -q --noproxy '*' --max-time 10 --header @A.header \
-o evidence/parameters/allowed.txt \
'http://127.0.0.1:8000/api/object-preview?object=O1'
curl -q --noproxy '*' --max-time 10 --header @A.header \
-o evidence/parameters/denied.txt \
'http://127.0.0.1:8000/api/object-preview?object=O2'
curl -q --noproxy '*' --max-time 10 --header @A.header \
-o evidence/parameters/forward.txt \
'http://127.0.0.1:8000/api/object-preview?object=O1&object=O2'
curl -q --noproxy '*' --max-time 10 --header @A.header \
-o evidence/parameters/reverse.txt \
'http://127.0.0.1:8000/api/object-preview?object=O2&object=O1'
For a JSON variation, preserve the literal repeated-name body in a file and use --data-binary @file with the correct content type; do not reconstruct it using a dictionary or UI form. curl data handling.
Report threshold and checklist
The report must identify the disagreeing components, raw ambiguous input, protected value checked, different value consumed, and controlled impact. A gateway-only rejection, intentional multivalue API, simple parser preference, or a client that changed the payload is insufficient. Use CWE-436 for an established interpretation conflict; add the specific resulting access or injection weakness where supported.
- Allowed and forbidden single-value controls recorded.
- Raw duplicates survived the test client.
- Parsed types, precedence and downstream construction recorded.
- Exact authorization/consumption mismatch demonstrated.
- Fixed-parser or fixed-encoding negative control compared.
Webhooks and integration events: authenticity, tenant binding and replayVerify the event signature, provider-to-tenant binding and durable worker effect together across retries and out-of-order delivery.
Open canonical module and reverse references
- 01Capture genuine event
- 02Verify signed bytes
- 03Bind provider and tenant
- 04Check replay and order
- 05Inspect durable effect
- 06Revoke and clean up
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and invariant
Treat an integration event as a separate privileged entry point. Inventory the subscription, receiving route, signing/authentication scheme, provider account, installation, tenant mapping, accepted event types, queue and final consumer. Use a local replica, a provider sandbox or local provider stub, two controlled tenants, and a reversible nonfinancial action. The invariant is: only an authentic, correctly bound event may cause its permitted effect, once under the workflow's rules.
Keep inbound event authenticity separate from outbound destination checks and object authorization. A valid signature does not itself identify which local tenant may be changed.
Numbered test procedure
- Capture a genuine baseline. Generate one ordinary event through the controlled integration. Save exact body bytes, verification headers, event/object IDs, provider account and mode, local tenant, queue ID and final state. Write down the expected change, such as granting one sandbox entitlement. Capture a correctly signed irrelevant event as a no-effect control. An HTTP
2xxcan acknowledge receipt before the worker runs. - Test authenticity before side effects. In the replica, compare the baseline with a missing signature, an invalid signature, and a one-byte body change retaining the original signature. Restore the local object, queue and deduplication fixture between authenticity trials: an already-consumed event can conceal a missing verifier. Follow both receiver and worker logs; rejection after a privileged item was accepted is too late. Respect the actual verification scheme. GitHub's documented HMAC covers payload bytes, so JSON reserialization changes the signed bytes; separately supplied event/delivery headers are not automatically covered. Prefer the official verifier and confirm failures stop processing. GitHub verification.
- Bind provider identity to the local object. Use valid sandbox events from two controlled provider accounts/installations. Verify each maps through trusted configuration to its correct tenant and provider object. For a local mutation experiment, change one untrusted routing field or unsigned header; a signed-field mutation requires an appropriately signed stub event and is a source-test hypothesis until its real attacker path is established. Check test/live mode and disconnected integrations. Do not claim a production forgery merely because the tester controls the lab signing key.
- Separate freshness, duplicates and order. Replay one valid event once and inspect durable effects. If the provider signs a timestamp, check the documented acceptance window using a local clock/fixture. A fresh signature is not a deduplication guarantee. Deliver two distinct legitimate events in reverse order in the sandbox and verify that an old update cannot undo a later revocation. Stripe documents duplicate delivery and does not guarantee event order; event creation seconds are not a safe total ordering or deduplication key. Use event identity and the product's object-state rules. Stripe webhook delivery behavior.
- Check dispatch and lifecycle. Compare the allowed event type/action with a genuine unsupported type and malformed-but-authentic local fixture. Ensure a loosely selected handler does not treat every event as success. Rotate a sandbox signing secret, record the intentional overlap, then check the old secret after that overlap ends. Remove the integration and test the documented handling of already queued events. GitHub recommends checking event type/action and keeps the delivery ID unchanged on redelivery. Treat any deduplication based on an unsigned header according to the actual authenticated binding. GitHub best practices.
- Inspect the transaction boundary. In the replica only, introduce one controlled worker failure before or after the effect, then retry. Check whether deduplication and the protected update can disagree, causing lost processing or duplicate effects. Use the existing state/replay procedure for transactional controls. Restore fixtures, cancel queued test deliveries, and remove local capture files when evidence has been redacted.
Command card: signed-body tamper control
Prepare event-valid.json and event-valid.headers from a newly generated local/sandbox delivery. The headers file contains only required verification and event metadata, not copied Host, Content-Length or production cookies. Prepare event-tampered.json by changing one known marker byte without re-signing. Start each authenticity case from the same clean local snapshot, including queue and deduplication state. Use a nonfinancial event and remain inside any signature freshness window.
mkdir -p evidence/webhooks
curl -q --noproxy '*' --max-time 10 \
--header @event-valid.headers -H 'Content-Type: application/json' \
--data-binary @event-valid.json \
-D evidence/webhooks/valid.h -o evidence/webhooks/valid.txt \
http://127.0.0.1:8000/webhooks/lab
# Inspect the valid effect, then restore the local fixture before this case.
curl -q --noproxy '*' --max-time 10 \
--header @event-valid.headers -H 'Content-Type: application/json' \
--data-binary @event-tampered.json \
-D evidence/webhooks/tampered.h -o evidence/webhooks/tampered.txt \
http://127.0.0.1:8000/webhooks/lab
For the separate replay experiment, restore once, send the valid request, wait for completion, and repeat it once without resetting. Inspect the durable effect count; two acknowledgements can correctly refer to one effect. The endpoint is illustrative. --data-binary preserves file bytes; these requests do not follow redirects. curl manual.
Report threshold and checklist
Report a forged protected transition, authentic event applied to the wrong controlled tenant, or replay/order failure with a durable security consequence. Acceptance of an intentionally unsigned public notification, permitted secret-rotation overlap, a harmless no-op acknowledgement, or a mock-only signing capability is not enough.
- Exact authenticated bytes and unsigned metadata distinguished.
- Provider identity, tenant, object and mode binding verified.
- Invalid/tampered controls caused no privileged worker effect.
- Freshness, duplicate delivery and order tested separately.
- Final state, retry behavior and cleanup recorded.
Azure and GCP: identity, delegation and storage accessApply provider-specific controls for Azure management/data permissions and SAS, plus GCP service accounts, federation and object ACLs.
Open canonical module and reverse references
- 01Pin provider and auth mode
- 02Map management and data grants
- 03Trace delegated identity
- 04Compare one canary
- 05Record propagation and clean up
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Define the provider-specific authority graph#
Keep the shared IAM procedure, storage procedure and authorization matrix. Add the provider's actual identity and permission system before transferring an AWS hypothesis. Use two owned principals, one allowed and one restricted, and one private canary per tenant. Record Azure tenant/subscription/resource IDs or GCP organization/project/resource names, selected CLI account and impersonation settings. An application tenant must not select another tenant's cloud identity or access path.
Azure: distinguish management, data and delegation#
Classify the caller. Record whether the application uses a user, service principal or managed identity. Map application tenant to that principal's object ID and allowed resource. Separate Microsoft Entra directory roles from Azure resource-role assignments. Inspect inherited assignments, groups, conditions and deny assignments using owner exports when visibility is incomplete.
Trace the real blob authorization route. A storage management role is not automatically a Blob data role when using Entra authentication. However, management permission to retrieve account keys can create a different data-access route if Shared Key is permitted. Preserve whether the request used Entra, Shared Key or a SAS; otherwise an apparent RBAC bypass may simply be a different credential. Test only the canary and reuse the identical auth mode for both controls. Azure Blob authorization
Identify the SAS type. Distinguish user delegation, service and account SAS. Record signed resource, allowed operations, validity interval and any stored-access-policy relationship before testing issuance. Ask tenant A for B's owned canary through the application, then verify only the issued capability's permitted synthetic operation. A correctly issued bearer SAS working after application logout is not automatically a flaw. Revocation mechanisms differ by SAS type; compare against the product's promise instead of assuming a uniform logout rule. Azure SAS overview
GCP: distinguish attachment, impersonation and storage grants#
Map service-account selection.
iam.serviceAccounts.actAssupports attaching an identity to an executable resource; it does not by itself grant general short-lived token creation. Token Creator permissions are a separate path. Inspect workload-create/update authority, selected service account and that account's actual resource access together. In a disposable fixture, compare one canary-only workload using the normal identity with the same supported operation using a more powerful owned identity. Do not create long-lived keys as a proof step. Service-account permission modelResolve inherited and object-level access. Record project/folder/organization grants and relevant restrictions. For Cloud Storage, determine whether uniform bucket-level access is enabled. When it is disabled, object ACLs can supply access independently of bucket IAM; when enabled, ACL operations are disabled. Compare the exact object and generation under the same account. A project policy export alone cannot establish every object-access path. Uniform bucket-level access
Inspect workload federation bindings. Verify issuer, audience, attribute mapping, attribute condition and target-resource/service-account binding. For a shared CI identity issuer, bind trust to the intended organization/repository and environment using the provider's documented claim semantics. Compare legitimate allowed and disallowed identities in an owned fixture. A valid issuer signature establishes token origin, not entitlement to every configured workload pool. Federation for deployment pipelines
Bounded command card#
These commands contact provider APIs. Run individually using prepared owned credentials. Supply approved literal IDs; inspect Azure storage environment variables and GCP impersonation configuration first. The Azure policy command avoids Microsoft Graph name lookups, but is still an authenticated management API request. It does not enumerate group membership or prove effective access. Azure role-assignment CLI
timeout 20 az role assignment list \
--subscription OWNED_SUBSCRIPTION_ID \
--scope /subscriptions/OWNED_SUBSCRIPTION_ID/resourceGroups/bounty-fixture \
--assignee-object-id OWNED_PRINCIPAL_OBJECT_ID \
--include-inherited --fill-principal-name false -o json
env -u AZURE_STORAGE_KEY -u AZURE_STORAGE_SAS_TOKEN \
-u AZURE_STORAGE_CONNECTION_STRING timeout 20 az storage blob show \
--subscription OWNED_SUBSCRIPTION_ID --account-name OWNED_STORAGE_ACCOUNT \
--container-name bounty-fixture --name canary.txt \
--auth-mode login --only-show-errors -o json
timeout 20 gcloud --configuration=bounty-lab-low \
storage objects describe gs://OWNED_BUCKET/bounty/canary.txt \
--format=json
--auth-mode login prevents the Azure CLI's legacy key-lookup route from changing the identity argument. The last two commands request metadata of one known fixture. Repeat under the intended allowed identity; metadata access does not itself prove the file contents were disclosed. A timeout or absent object is not a permission denial. Azure Blob CLI, GCP object describe
Report and cleanup checklist#
- Provider, tenant/project, principal and selected authentication mode recorded.
- Management, data access and delegated workload authority separated.
- Inheritance, ACLs, conditions and visibility gaps documented.
- Allowed and denied controls address one identical synthetic resource.
- SAS/federation claims redacted; usable tokens excluded from evidence.
- Documented propagation delays considered before claiming revocation failure.
- Disposable workloads, object generations, temporary grants and shares cleaned up.
Preserve the precise failed edge and request IDs. A powerful administrator, intended delegation, stale session or deliberately public object must remain separate from an application tenant escaping its assigned authority.
Browser extensions: message authority and document lifetimeTrace real page messages into extension capabilities and test sender identity, navigation races, permission changes and worker restarts.
Open canonical module and reverse references
- 01Identify installed release
- 02Map message channels
- 03Capture allowed action
- 04Change sender or document
- 05Confirm recipient and effect
- 06Restore profile
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and security invariant#
Use the scoped store release, a disposable browser profile and two controlled local web origins. Record extension ID/version, browser version, Manifest version and granted optional permissions. Keep a separate unpacked copy for source tracing; changing its manifest, ID or permission grants changes the reproduction assumptions. Seed one private marker and one public marker in the extension's supported lab data. Invariant: a page cannot borrow extension or native-host authority beyond the specific operation delegated to that page and document.
Procedure, controls and interpretation#
Separate the message channels. Draw page → DOM event/content script → service worker → browser API/native host. Inventory
window.postMessage, custom events,runtime.onMessage, external-message listeners and ports separately. An external-message allowlist does not control an ordinary content script forwarding page data. Use only the channel actually reachable from a normal page. Chrome documents that content scripts are a lower-trust context for extension messages. Message passing, external messaging configuration.Capture a legitimate request. Trigger one supported page action and record action name, input schema, browser-provided sender ID/origin, tab/frame/document and the protected operation. Distinguish browser-supplied metadata from fields named
origin,sender,tabIdortrustedinside the message body. A content-script sender ID identifying the extension does not establish that its forwarded page input was authorized. Keep sensitive values out of the page until its entitlement is established.Vary one authority dimension. Compare the valid page with the second controlled origin, a child frame, an unsupported action and a different known synthetic resource. Invoke through the product's real page listener; do not paste privileged extension APIs into its developer console. For a fetch broker, compare the intended public fixture with the protected marker destination and inspect who receives the response. Chrome's network guidance specifically warns against letting an untrusted sender choose arbitrary fetch destinations. Reuse browser trust and SSRF boundaries for URL normalization and redirect decisions. Extension network requests.
Test document lifetime. Pause a local mock response, navigate the initiating tab to the second origin, then release the response. Observe whether the reply or permission decision follows the original document or merely a reused tab/frame. Include same-tab navigation and a child-frame case separately. Chrome exposes
documentIdandoriginon message senders; those fields need browser-version-aware handling. The inference being tested is whether the product preserves the identity it authorized across an asynchronous gap. A dropped reply is a valid denied control. MessageSender, document-specific tab messaging.Restart and revoke. Let the service worker stop naturally or stop it through the local debugging UI; repeat the denied case after restart. Separately revoke an optional permission through supported browser controls. Record whether trusted state, grants or pending replies are re-created safely. Service-worker global variables are not durable storage. A debugging session can alter worker lifetime, so repeat the minimum case with developer tools closed. Service worker lifecycle.
Confirm the capability. Record the actual returned marker or reversible operation. For native messaging, continue with desktop IPC; distinguish extension-to-user-process access from a demonstrated administrator boundary. Broad permissions, a public resource, or a rejected unknown action are not standalone findings.
Local command sheet#
These inspect an acquired lab copy without loading it or invoking browser APIs:
python3 -I -S -m json.tool ./extension/manifest.json
rg -n 'postMessage|addEventListener|onMessage|onConnect|externally_connectable' ./extension
rg -n 'sender\.|documentId|frameId|tabs\.(sendMessage|connect)|sendNativeMessage' ./extension
rg -n 'fetch\(|host_permissions|optional_host_permissions|storage\.(local|session)' ./extension
For each match, save an evidence row: initiating origin/document, message fields, trusted metadata, decision, receiving document and resulting capability. The source scan identifies routes to exercise; a match itself proves no reachability.
Completion checklist#
- Allowed and denied origins produce the expected baseline decisions.
- The candidate works through the shipped page/content-script entry.
- Body-supplied identities are distinguished from browser metadata.
- Navigation and restart results include their event order.
- Optional permission grants and user interaction are documented.
- Remove local page fixtures and restore the disposable profile after retaining redacted evidence.
AI state: memory, resumed work, retries and private cachesTest persistent context, task ownership, MCP request-state integrity, duplicate effects and caches across identities and time.
Open canonical module and reverse references
- 01Seed owned state
- 02Verify owner controls
- 03Change principal or lifetime
- 04Resume or retry once
- 05Inspect committed effects
- 06Clear fixtures
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and security invariant#
Use a local agent/backend with two owned principals A/B, a persistent test database, a deterministic mock model and a stub tool that only appends a marked event. Record agent/framework version, protocol version, connector identity and task contract. Keep budgets to one operation per case and at most two delivery attempts. Invariant: stored context, delayed work and retries preserve the authorized principal, operation and permitted lifetime. Use tool approvals for initial consent, RAG isolation for documents and resource budgets for cost bounds.
Operational procedure#
Inventory persistent objects. List conversation, checkpoint, durable memory, task/job, result artifact and connector credential separately, with owner and authorized readers. Thread history and cross-thread memory can live in different stores; LangGraph explicitly distinguishes these persistence systems. Seed a random private B marker, absent from A's prompts and fixtures. Confirm both owners can read their own state and A cannot directly read B's. LangGraph persistence.
Exercise state handles below the model. Compare A's own conversation/checkpoint with a known B-owned handle using A's same authenticated request. Follow read, resume, export and delete independently; successful queue admission is not proof that work completed. Repeat a resume after removing membership and after switching the active account, observing the worker's effective principal. A scheduler's service account must not erase the narrower authority of the initiating job. Report a stored-object authorization defect independently of any prompt injection.
Track memory provenance. Store one explicitly approved harmless preference as a positive control. Introduce an alternate preference only through a lower-trust retrieved fixture. Start a fresh task and inspect whether that content was promoted into trusted instructions or another tenant's memory. Use a stub destination change as the protected effect. A legitimate user-approved memory or changed answer style is not evidence of unauthorized persistent control. Record the original source and each summary/handoff, then remove the memory and confirm the effect stops. This is a proposed experiment for the product's persistence contract, not a claim that every memory feature is vulnerable.
Bind resumed MCP requests. For implementations using 2026-07-28 MRTR, compare an untouched
requestStatewith one altered local copy, a B-owned state under A, an expired state and the same state with changed action arguments. Observe a harmless protected result. The specification requires integrity checks where state affects authorization or business logic; it recommends principal, expiry and originating-request binding. Single-use effects require server-side enforcement beyond a protected token. These controls apply to the server, even though a conforming client treats the state as opaque. MCP MRTR.Disconnect at a defined point. In the local adapter, commit one marked stub operation and simulate loss of its response, then retry once. Compare committed events with received responses. A fresh JSON-RPC ID identifies a request, not automatically a business-level idempotency key. Require at-most-once behavior only when the operation promises it; intentionally repeatable actions need a different invariant. Also cancel before commitment and verify the documented cancellation boundary. The HTTP protocol removes stream redelivery, so transport reconnect behavior must not be mistaken for proof of business deduplication. MCP HTTP transport.
Compare private caches. Warm a synthetic private resource as B, then request the same method/parameters as A. Compare cold and warm results and each authorization context. MCP private results cannot be shared across authorization contexts; MRTR retries carrying input/state are not cacheable. Record protocol hints and actual returned data, since hints alone do not enforce object access. An identical public tool list is expected. MCP caching.
Bounded command and ledger sheet#
rg -n 'thread_id|checkpoint_id|requestState|inputResponses|cacheScope|idempotency' ./src
python3 -I -S - <<'PY'
import collections, json
events = collections.Counter()
with open('lab/state-events.jsonl') as stream:
for number, line in enumerate(stream):
if number >= 1000:
raise SystemExit('Fixture exceeds 1000 event review limit')
row = json.loads(line)
if row.get('committed'):
events[(row['case'], row['principal'], row['operation_id'])] += 1
for key, count in sorted(events.items()):
print(*key, 'commits=', count)
PY
The ledger is your locally recorded fixture output, not a scanner result. Include case, principal, operation_id, committed, timestamp, authorization decision and state/argument digests. Never log usable credentials.
Evidence and cleanup#
- Owner, cross-owner, expired/revoked and valid-resume cases are recorded.
- Committed effects are counted independently of model text and responses.
- Memory source and receiving principal survive every transformation.
- Cold/warm cache evidence identifies the actual reader.
- Restore the test database, delete canaries/checkpoints and revoke fixture credentials.
Resource budgets and sensitive business flowsTrace request fan-out, retries, recipient limits and tenant quotas using tiny local limits, recording stubs and measured downstream effects.
Open canonical module and reverse references
- 01Name the protected resource
- 02Prepare recording stubs
- 03Establish limit controls
- 04Vary one budget dimension
- 05Count committed work
- 06Restore and retest
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Identify the unit that must be bounded#
An HTTP request can fan out into database queries, recipient notifications, reservations, image conversions, token generation, or recurring background work. Name the scarce resource and the authority that should own its budget: actor, tenant, recipient, workflow, or global service. A request-per-second limit does not necessarily constrain those downstream units. OWASP distinguishes unrestricted resource consumption from excessive use of sensitive business workflows; both need a concrete product-specific impact. API4, API6.
Build a small cost map before a test. For each entry point record accepted input size, batch cardinality, query/page complexity, queued jobs, retries, output size, and billed provider calls. Include failed requests: validation after a notification or job enqueue can return an error after consuming the protected resource. Use the state and business logic module for one-time transitions and the API protocol module for per-operation behavior.
Controlled procedure#
- State the business rule. Specify the prohibited outcome, such as more than two notifications to one synthetic recipient per lab window, or a member reserving an object after their allocation is exhausted. A missing CAPTCHA or rate-limit header alone is not the outcome.
- Prepare a faithful local deployment. Replace email, SMS, payments and model providers with recording stubs. Use supported configuration to reduce a real limit to a small lab value when possible. Record the configuration difference. Do not introduce a new guard or a fabricated endpoint and claim it represents the original application.
- Establish accepted and rejected controls. Exercise a permitted request and one documented over-limit case. Inspect actual downstream events and final state alongside the response. If the protected limit does not exist, bound the observation through source analysis and an isolated fixture; do not seek a production outage.
- Change one dimension. Compare a single operation with a small batch; the same actor through two permitted entry points; an original event with a duplicate; or a queued operation before and after its reservation expires. Keep the recipient, tenant, and resource ownership under your control. Test concurrency only when required to distinguish a non-atomic counter, using a local two-worker harness.
- Measure work and isolation. Record request count, accepted operation count, provider-stub count, bytes, task retries, and the budget's key. Check whether tenant A can consume tenant B's allocation or cause B's valid request to be rejected. A shared quota can be intentional; use the stated product rule.
- Reset and minimize. Cancel pending jobs, release synthetic reservations, clear only the dedicated fixture's state, and replay the smallest sequence. Recheck that a legitimate operation still works after the counter expires or allocation is restored.
Local command card#
The following is an adaptable request recipe, not a runnable application. Prepare lab/baseline.json, lab/at-limit.json, and lab/over-limit.json using the product's real request schema; create a header file with a synthetic lab token. Replace /api/exports with the reviewed local endpoint. Confirm the application uses recording stubs and that all three cases fit the agreed tiny experiment before running it.
mkdir -p evidence
for lab_case in baseline at-limit over-limit; do
curl -q --noproxy '*' --proto '=http' \
--connect-timeout 2 --max-time 10 --max-filesize 65536 \
--request POST --header @lab/authorization.header \
--header 'Content-Type: application/json' \
--data-binary "@lab/${lab_case}.json" \
--dump-header "evidence/${lab_case}.headers" \
--output "evidence/${lab_case}.body" \
'http://127.0.0.1:8000/api/exports' || break
done
This sends at most three sequential requests and does not follow redirects. It does not reset state, enforce the server's budget, or prove a limit. Capture the application's worker and stub logs separately. A transport failure stops the loop and must be recorded as blocked; HTTP error responses remain available for interpretation. If each case requires a fresh counter, restore the fixture between cases instead of running the loop as written. curl options.
Evidence, false positives, and completion#
Keep the expected budget, configuration, identities, input sequence, counter key, downstream effects, and valid control together. Demonstrated local resource amplification may support an impact argument; projected production cost remains an extrapolation until deployment and pricing assumptions are established. A few accepted requests below an unknown threshold do not prove unlimited access. A slow response is not proof of denial of service, and standard product automation may be intended.
- The protected resource or business allocation and intended owner are explicit.
- Real integrations are stubbed; no uninvolved recipient, customer, or bill is affected.
- The accepted control and over-limit control use the same relevant configuration.
- Downstream work is counted even when the HTTP response indicates rejection.
- Batch size, retries, async processing, and identity/tenant budget keys are considered.
- The minimal observation and any cost extrapolation are clearly separated.
- Pending work and fixture state are restored, with a legitimate operation retested.
Vulnerability reference#
Broken object-level authorization (BOLA / IDOR) · Broken function-level authorization (BFLA) · Broken property authorization: private response fields · Mass assignment of server-controlled properties · Authentication or MFA state bypass · Session fixation across login · Residual access after explicit session revocation · Password reset binding or single-use failure · OAuth login or account-linking confusion · JWT signature, issuer, audience, or token-purpose validation failure · SQL injection · NoSQL operator or query injection · OS command injection · Server-side template injection (SSTI) · Reflected or stored cross-site scripting · DOM-based cross-site scripting · Cross-site request forgery (CSRF) · CORS private-data exposure · Cross-origin messaging trust failure · Server-side request forgery (SSRF) · Unsafe upload interpretation or placement · Path traversal outside an allowed root · XML external entity injection (XXE) · Archive extraction path escape · Web cache poisoning · Web cache deception · HTTP request smuggling / desynchronization · Race condition in a security-sensitive transition · Workflow order or approval bypass · Single-use action or idempotency replay failure · GraphQL resolver or field authorization failure · WebSocket message or subscription authorization failure · Cross-site WebSocket hijacking · Unsafe object deserialization · JavaScript prototype pollution · Parser resource exhaustion and decompression amplification · Regular-expression denial of service · Anonymous access to private object storage · Unauthorized object creation or replacement · Signed storage capability exceeds its authorized scope · Private delivery policy bypassed through the origin · Dangling DNS reference to a retired service · Agent tool accepts unauthorized object or tenant parameters · MCP accepts a token for another resource · RAG retrieval or citation resolver crosses tenant boundaries · RAG cache or fresh retrieval ignores revoked access · Open redirect across an origin trust boundary · Clickjacking of a sensitive authenticated action · Untrusted Host or forwarding-header origin construction · Sensitive data in errors, debug routes, or generated files · Predictable security token or capability identifier · Password storage without an adequate adaptive hash · Encrypted data accepted without integrity authentication · SAML assertion signature, audience, or response binding failure · Unauthorized authenticator enrollment, replacement, or removal · WebAuthn assertion accepted for an invalid account or ceremony context · Verification or account merge preserves another actor's access · Enforced SSO bypass through an alternate login path · Argument or option injection without shell interpretation · Parameter or JSON parser disagreement crosses a security check · Server-side parameter pollution in a constructed request · Unverified or tampered webhook causes a protected effect · Authentic integration event applied to the wrong tenant or object · Page message borrows unintended browser-extension authority · Delayed extension result reaches a different document · Agent conversation, checkpoint or memory crosses ownership boundaries · Agent retry commits a protected single-use action twice · Unbounded resource allocation or request amplification · Sensitive business flow exceeds its permitted frequency or allocation
1. Turn scope into executable boundaries#
Record an authorization tuple for every candidate: program + asset identifier + owner + environment + scheme/protocol + port + path + permitted accounts + permitted techniques + exclusions + policy timestamp. Keep discovered, ownership-attributed, authorized, reward-eligible, and tested as separate states. A CNAME, certificate, repository reference, ASN association, or company logo proves none of the later states by itself.
The following interpretations deliberately avoid silently expanding a program's words. A program-specific definition can broaden or narrow any row.
| Scope expression | Working interpretation | Gate before expansion |
|---|---|---|
Exact URL https://app.example.com/report |
Listed scheme, host, port, and resource | Confirm whether query variants, child paths, and sibling routes are covered |
Path prefix https://app.example.com/api/v2/ |
Listed origin and documented path subtree | /api/v20, /admin, redirects, alternative encodings, and rewritten paths require independent evaluation |
Exact hostname api.example.com |
That hostname, with the program's permitted protocols/ports | Neither siblings nor its resolved IP as a general network target follows automatically |
Apex example.com |
The apex hostname | Subdomains are separate unless expressly included |
Wildcard *.example.com |
Hostnames matching the policy's wildcard definition | Do not assume apex inclusion, unlimited depth, every port, or third-party hosted services |
IP 192.0.2.10 |
This IP and allowed services | No automatic entitlement to other IPs, other virtual hosts, provider control planes, or every tenant |
CIDR 192.0.2.0/28 |
Addresses within the prefix, subject to exclusions | Verify ownership and service restrictions; no ASN-wide expansion |
Port :8443 / TCP / UDP |
The specified transport and port | TCP/443 permission does not imply UDP/443, SSH, SMTP, or an origin backend |
| Staging, demo, or sandbox | Named deployment and test account context | Shared identity, payment, telemetry, and production backends remain separate boundaries |
| SaaS tenant / workspace | Authorized tenant and stated cross-tenant test setup | Other customers and vendor infrastructure are not automatically authorized |
| Repository or self-hosted product | Eligible code/version and your controlled deployment | Public deployments of that software need their own authorization |
Implement exclusions before inclusions and fail closed on malformed URLs, credentials in URLs, ambiguous encodings, unsupported schemes, and unresolved ownership. Compare parsed hosts, not substring matches: example.com.attacker.test is unrelated to example.com. Normalize IDNs and trailing DNS dots consistently. For path-limited scope, record the exact request target and any proxy normalization; do not let an allowlist's normalization disagree with the client that sends the request. Recheck each redirect and every discovered URL before sending a new request.
Use WSTG for testing procedures and ASVS for expected controls. Pin references: the WSTG project identifies 4.2 as a released version and 5.0 as development; ASVS identifies 5.0.0, released 30 May 2025. Neither framework makes every failed control a payable bounty. OWASP WSTG, OWASP ASVS.
2. Recon that produces hypotheses#
Pass 1 — third-party discovery. Read program documentation, public API references, repository references, certificate-transparency results, historical URL indexes, and already collected search-engine/network-index records. Save source and collection time; stale records are leads. Searching an existing Shodan/Censys record is different from requesting a fresh scan. Third-party discovery can disclose the target name to a provider even when it does not probe the target.
Pass 2 — allowlist review. Resolve candidates into exact permitted origins/resources. Do not pipe passive output directly into a scanner. Retain excluded and uncertain candidates in a review queue. Separate broad wildcard inventory from a single-app campaign: the latter needs route and workflow discovery more than another million hostnames.
Pass 3 — active validation. DNS resolution is a network operation and recursive resolution may reach authoritative infrastructure. HTTP requests, TLS handshakes, screenshots, crawling, directory discovery, port scans, DNS brute force, and vulnerability checks are active. Use the program's identification header and rate limit. If no rate is specified, begin manually; a conservative automation starting point is one worker and at most one request/second, only where automation is permitted. This is a proposed budget, not a universal authorization.
Pass 4 — application mapping. Exercise normal workflows through an intercepting proxy. Inventory method, path, operation name, object IDs, role, tenant, side effects, upload/download behavior, integrations, and request source. Compare UI traffic with OpenAPI schemas, SDKs, JavaScript and source maps. Old versions, undocumented operations, export jobs and background tasks become hypotheses, not automatic findings.
# Third-party discovery only: do not add -active/-nW.
# Review installed config and available provider credentials first.
subfinder -d example.com -silent -o candidates.txt
# Human-reviewed file: exact URLs, explicit schemes and permitted ports.
# Use a reviewed empty YAML config to prevent inherited scan options.
httpx -config scope-httpx.yaml -l approved-origins.txt \
-nfs -sc -title -location -json -threads 1 -rate-limit 1 \
-retries 0 -timeout 10 -disable-update-check -o inventory.jsonl
# Single approved URL; -q first ignores inherited curlrc options.
# No -L/--location: inspect any Location before a separate request.
curl -q --proto '=https' --connect-timeout 5 --max-time 15 \
--dump-header evidence/headers.txt --output evidence/body.txt \
'https://app.example.com/approved-path'
scope-httpx.yaml should contain {}; evidence/ must already exist. These commands assume tools are installed. httpx here means ProjectDiscovery's binary. Its -nfs preserves the input scheme; -nf has different behavior. Omit redirect-following, screenshot, TLS-domain, CSP-domain, favicon, additional-port, and extra-path probing flags. -fhr limits redirects to the same host, not to the same port/path. -allow/-deny are IP/CIDR controls, not URL scope enforcement. For exact-path scope, prefer a reviewed single request until client behavior is confirmed. Subfinder usage, httpx usage, curl manual.
Automated vulnerability checks come after mapping. Review each selected template's requests, methods, payload count, redirects, extracted targets, code/headless behavior, callbacks and cleanup. A severity label is not a safety classification. A low rate does not make a destructive request safe. Do not use “run all templates” as a discovery stage.
3. Authorization: two identities, two tenants, controlled objects#
Create accounts A and B and tenants T1 and T2 where permitted. Initially A owns T1 and B owns T2. Seed distinguishable synthetic objects O1 and O2. For within-tenant checks, temporarily add B to T1 with a documented limited role, then revoke that membership. Use separate browser profiles and token stores. Additional administrator identities improve coverage when available, but are not required to start.
Record expected behavior from documentation and normal use before testing. “Not visible in the UI” alone does not establish a forbidden action. Different status codes are not proof: inspect response data, resulting state, audit events, and eventual background-job output. PortSwigger access control.
| Actor/context | Resource/action | Expected result to validate |
|---|---|---|
| Anonymous | Private O1 read/export | No private content or downloadable artifact |
| A in T1 | O1 normal operations | Authorized baseline succeeds |
| B, T2 only | O1 read, update, delete, share | Denied across tenants |
| B as T1 viewer | O1 modification, invite, billing, token creation | Restricted by role |
| B as T1 member | A's explicitly private object | Restricted by ownership or sharing policy |
| B after removal | Old token, existing socket, queued export | Access follows documented revocation semantics |
| A using T2 identifier | O2 nested resource and batch operation | Server checks tenant/object relation |
| A with restricted API key | Administrator function or forbidden field | Key permissions apply independently of UI permissions |
For each operation, test three independent dimensions: object (whose record), function (which action), and property (which fields may be read or written). Substitute only known IDs from your controlled accounts; include IDs in paths, JSON bodies, query parameters, headers, nested objects, arrays, export IDs, download tokens and WebSocket subscriptions. Verify list/search/count routes as well as direct reads. Do not enumerate strangers' IDs to demonstrate scale.
Report threshold: a reproducible violation of the intended permission boundary on controlled data. Save a baseline request, the minimally modified request, both identities' roles, expected rule, response/state evidence and cleanup. Stop: once the first unauthorized read/write is proven; do not turn one record into a mass export. If real customer data appears, preserve the minimum necessary metadata and stop that path.
4. Protocol branches#
| Surface | Focused checks | Evidence and stopping rule |
|---|---|---|
| REST / JSON | Methods, versions, nested resources, server-owned fields, batch operations, pagination, alternate content types | Compare two controlled identities; prove changed server state or private output, not merely HTTP 200 |
| GraphQL | Resolver and field authorization, nodes/global IDs, mutations, subscriptions, aliases with tiny batches | An exposed schema alone is usually a lead; prove forbidden data/action. Avoid depth/complexity exhaustion |
| gRPC | Known .proto methods, channel versus per-call credentials, tenant metadata, unary/stream authorization |
Capture method, metadata and gRPC status/trailers; HTTP 200 alone says little. Reflection alone is not privilege escalation |
| WebSocket | Handshake authentication, Origin handling, per-message authorization, room subscription, logout/revocation | A controls the sender, B the recipient. Prove unauthorized subscription/action with a marker; stop after one message |
GraphQL introspection helps map a schema, but public APIs may intentionally expose it. Prefer observed operations and small schema queries before broad introspection; authorization still belongs at the resolver/object/field boundary. PortSwigger GraphQL.
gRPC distinguishes channel credentials from call credentials, and reflection exposes service descriptors for tooling. Test the real trust boundary rather than reporting the existence of either mechanism. Avoid assuming a token valid for one service may be forwarded to another. gRPC authentication, gRPC reflection.
WebSocket checks must cover messages after the handshake, including existing connections after membership changes. Cross-site WebSocket hijacking requires a browser-feasible connection and meaningful unauthorized interaction; an accepted arbitrary Origin header in a proxy is insufficient by itself. PortSwigger WebSockets.
5. Authentication, federation, and session lifecycle#
Build a state diagram in notes: registration → email verification → login → MFA → sensitive reauthentication → recovery → account linking → logout/revocation. Test alternate entry points, mobile/API clients, and identity changes using your own inboxes. Check that a password reset changes the intended account, consumed tokens cannot be replayed, and email changes do not silently inherit an old verified identity.
For sessions, compare pre-login and post-login identifiers, privilege-change rotation, inactivity/absolute expiry, logout, reset, MFA change, device removal, API-key revocation and tenant removal. Distinguish browser logout from server-side invalidation; assess residual validity against promised behavior and impact rather than assuming every token must instantly disappear. OWASP Session Management.
For OAuth, test authorization-response binding to the initiating browser, exact approved redirect handling, authorization-code reuse, PKCE behavior, account-linking confirmation and issuer mix-up boundaries. Missing state alone is not conclusive if another valid binding mechanism exists. RFC 9700 is the current OAuth security BCP, published January 2025. RFC 9700, PortSwigger OAuth.
For OIDC, verify signature trust, issuer, audience, expiry and nonce when applicable; distinguish an ID token identifying a user from an access token authorizing API use. For SAML in an authorized test identity provider/service provider setup, examine signature coverage, trusted certificates, audience/recipient/destination, request correlation and assertion replay. Report threshold: demonstrate login, linking, privilege or session access as the wrong controlled identity. Stop: no identity-provider-wide attacks or takeover of real users. OpenID Connect Core, OWASP SAML Security.
6. Business logic and bounded concurrency#
Write invariants before payloads: “Only an owner can transfer ownership”; “one invitation creates one membership”; “a cancelled subscription cannot create new privileged credentials”; “a discount applies once”; “an export belongs to the requesting tenant.” Trace intermediate states and repeat, reorder, skip or replay one transition at a time. Include asynchronous workers, webhook delivery, stale links, retries, negative/zero quantities, precision/currency conversion, and price inputs where the program provides a nonfinancial sandbox. PortSwigger business logic.
For a race, first run the sequence serially and record the expected state. Preconditions: explicit permission for concurrency, a reversible test object, a way to inspect durable state, and no real money/inventory impact. Start with two synchronized requests and a fixed small experiment budget; do not assume this budget is safe for every endpoint. Compare with serial controls and inspect committed state after jobs settle. Two success responses may still represent one committed operation. Report threshold: a duplicate transition or violated invariant that survives completion. Stop: at first proof, increased errors, queue growth, or unintended side effects; restore your test object. Practice timing techniques locally before production. PortSwigger race conditions.
7. Focused vulnerability test cards#
Each card assumes the exact endpoint and technique are authorized, a baseline exists, and synthetic data is available.
| Class | Bounded action | Evidence / report threshold | Stop or defer |
|---|---|---|---|
| SQL / NoSQL injection | Change one input with paired controls; confirm input reaches a query/operator context | Reproducible query-semantic change tied to input; syntax errors alone remain leads | No database dumps, destructive statements, or unbounded timing probes |
| Command / template injection | In a local replica first, use a harmless expression/marker appropriate to the sink | Distinguish template evaluation from client rendering and reflection; show relevant execution impact | Stop at minimal proof; no shell persistence or environment-secret collection |
| XSS | Determine HTML/attribute/JS/DOM context; demonstrate a benign marker executing for controlled B | Another permitted user's browser executes in the affected origin with realistic delivery | No cookie/token exfiltration; avoid content visible to real users |
| Upload | Test filename, content type, file content, ownership, retrieval and processing independently | Unauthorized controlled-file access, unsafe interpretation, traversal, or executable handling | No web shells, overwriting application files, archive bombs, or expensive documents |
| Path traversal | Use an owner-provided canary or file planted in your local replica outside the allowed directory | Read/write outside intended path boundary with a known harmless file | Do not retrieve production configuration, keys or user files |
| SSRF / XXE | Send a unique callback to infrastructure you control; separate DNS from HTTP observations | Show unintended server-side request and reachable policy boundary; intended URL fetching is not automatically SSRF | No cloud credential retrieval, internal sweeps or unrelated destinations; ask owner for an internal canary if needed |
| CSRF | From your controlled origin, make B's real browser perform one reversible protected action | Ambient credentials plus attacker-deliverable request cause unauthorized state change | Missing token alone is insufficient; do not involve real users |
| CORS | From controlled origin, read a private response in B's browser with realistic credential behavior | Browser actually exposes sensitive controlled data cross-origin | Access-Control-Allow-Origin: * with public content is not a finding; wildcard plus credentials is not automatically browser-exploitable |
| Cache poisoning / deception | Use a demonstrably isolated cache key and controlled marker/accounts | Another controlled client receives poisoned content or A's private data from shared cache | Unique query strings may be ignored in cache keys; stop if isolation is uncertain |
| Request parsing / smuggling | Reproduce proxy/backend disagreement in a lab or program-approved isolated endpoint | Demonstrate a request-boundary mismatch with controlled paired traffic | Production desynchronization can affect unrelated users; no broad probes or response-queue capture |
Technical references for the respective cards: SQL injection, NoSQL injection, template injection, XSS, uploads, path traversal, SSRF, CSRF, CORS, cache poisoning, request smuggling.
8. Network, DNS, TLS, and email branches#
Use the dedicated Network services playbook for the full DNS, TLS, email, database, queue, VPN and appliance workflow.
Only use this branch when the service/IP/port is covered. A website scope does not automatically authorize its hosting provider's network. An open port identifies a reachable service, not a vulnerability; an observed version can be backported, masked or misidentified. Nmap service detection sends protocol probes and can have side effects; even its documentation excludes printer port 9100 by default for that reason. Select individual services before deeper interrogation. Nmap port states, Nmap version detection.
For DNS, inspect A/AAAA/CNAME/NS/MX/TXT relationships. Public DNS lookups differ from passive historical DNS searches. A dangling CNAME is evidence of a configuration lead, not proof that the referenced resource is claimable. Resource claiming can change production routing; obtain the program-approved proof method before registration. Zone transfer, dynamic updates, recursion abuse and amplification testing require a specific allowed branch.
For TLS, connect using the intended SNI and verify the intended hostname; a wrong default certificate from an IP-only connection may simply reflect virtual hosting. Certificate SANs provide candidates, not added scope. Avoid treating a scanner's “old cipher” label as meaningful impact without the program's eligibility criteria. OWASP TLS.
# Active DNS queries; only after the applicable discovery gate.
dig +time=2 +tries=1 app.example.com A
dig +time=2 +tries=1 app.example.com AAAA
# Active handshake to an approved hostname and port.
timeout 15 openssl s_client -connect app.example.com:443 \
-servername app.example.com -verify_hostname app.example.com \
-verify_return_error -brief </dev/null
The DNS and TLS commands use their official documented query/connection options. Hostname verification and SNI are separate controls. BIND manual, OpenSSL s_client.
For SMTP/IMAP, distinguish the domain's mail configuration from an authorized mail-server service. Review SPF/DKIM/DMARC findings against program exclusions and actual spoofing/identity impact. Any relay or delivery test needs explicit scope and researcher-controlled mailboxes; do not send messages to staff to prove delivery. An MX pointing to a provider does not authorize that provider. For SSH, databases, queues, object stores and administration panels, start with service identity and authentication requirements; never jump from a banner to default-password spraying or reading datasets.
9. Exit checklist#
- Policy snapshot and exact asset/technique authorization recorded; exclusions applied.
- Every candidate has an ownership and scope decision; redirects and dependencies reviewed.
- Synthetic accounts, two tenants, role transitions and cleanup plan established.
- Endpoints include background jobs, exports, integrations and alternate protocols.
- Object, function and property authorization checked independently.
- Authentication and revocation tested across UI/API/socket paths.
- High-value business invariants have serial controls before concurrency tests.
- Browser findings verified in a real browser; server findings have controlled differential evidence.
- Finding has a reproducible boundary violation, not only a scanner label or missing hardening control.
- Sensitive evidence minimized; timestamps, requests, relevant responses and state changes preserved.
- Test records removed, tokens revoked, callbacks stopped and report submitted privately.
Identity and protocol coverage review — 14 September 2026#
The shared procedures now distinguish account lifecycle, authenticator proof, federation trust, and API transport. Complete the applicable branch for the deployed feature; unavailable access is an untested branch, not a passed check.
| Observed feature | Additional checks | Reusable procedure |
|---|---|---|
| Email verification, invitations, account merging | Pending identity, stale links, cancelled invitations, retained sessions | Identity lifecycle |
| MFA, passkeys, recovery codes | Factor binding, replacement, replay, verification policy | Authenticator lifecycle |
| OAuth, OIDC, SAML | Transaction binding, token purpose, issuer/audience, signature coverage | Federation and token trust |
| Enterprise SSO and SCIM | Alternate login paths, tenant provisioning scope, group removal and revocation interval | Identity lifecycle |
| HTTP/JSON-to-gRPC or gRPC-Web gateway | Same method and object through each deployed authentication path | Protocol parity |
| SSE and resumed streams | New-event authorization, reconnect cursor, membership removal | Protocol parity |
Start with a legitimate request, change one identity/proof/context, inspect a protected result, and preserve a control. Follow the module's full procedure before reaching its command card. The new classes are indexed with reverse references in the vulnerability catalog.
- Enrollment and recovery cannot silently reduce a required authentication guarantee.
- Account verification or linking does not preserve unintended access.
- An enforced SSO policy is checked at protected tenant access.
- Token purpose and claim checks are distinguished from signature checks.
- GraphQL alternate methods, deployed gateways, and resumed streams preserve permissions.
Follow the two commonly missed server entry points#
The server-side review now includes request-value interpretation and webhook event integrity. These are reusable across web/API, self-hosted OSS, cloud services and AI integrations. Run them when the application has multiple parsing layers, internal service calls or external event delivery; record an inapplicable result with the reason when those features are absent.
| Boundary | Minimal controlled comparison | Required evidence |
|---|---|---|
| Duplicate query/JSON values | Allowed scalar, denied scalar, both values in each order | The value checked differs from the value used in a forbidden operation |
| Downstream parameter construction | Literal marker, delimiter marker, correctly encoded control | Actual internal request parameters plus changed policy semantics |
| Webhook authenticity | Genuine event, invalid signature, changed body with unchanged signature | No protected effect for invalid cases, or the exact unauthorized worker transition |
| Provider-to-tenant mapping | Valid events from two controlled integrations | Authenticated provider identity tied to the correct local tenant/object |
| Retry and event ordering | Serial replay and two valid events in reversed order | Durable effect count and final permissions after workers settle |
| Delayed injection | Store marker, trigger observed export/job, compare literal control | Both entry and later sink, including the stored bytes |
Keep ordinary parser preference, delivery acknowledgement and cached responses separate from demonstrated security failures. Use the modules' numbered procedures and linked vulnerability cards for report thresholds and false-positive controls.
- Parser/type/precedence behavior checked where multiple layers consume input.
- Later consumers of stored input traced where exports or workers exist.
- Incoming events authenticated before privileged processing and bound to tenant/object.
- Replay, ordering and queue effects inspected against the documented workflow.
- File links, cache revalidation and actual SSRF connection identity checked when applicable.
Review additions verified against primary documentation on 14 September 2026. Earlier chapter references retain their original review dates; these additions do not imply that every external link or every target was tested.
Source registry#
All sources accessed 2026-09-11. “Undated” means no reliable publication date was established; it does not imply old content. Sources were read directly. This chapter's test budgets, matrices, ordering and stopping rules are synthesis, not quotations from those sources.
Review update, 14 September 2026: the official OWASP API Security Top 10 2023 overview, BOLA and object-property authorization pages were successfully retrieved. Original source-registry access dates remain historical.