OSS repositories and self-hosted products
Repository hunting succeeds when source access becomes a reproducible explanation of an attacker crossing a security boundary. An unsafe function, dependency advisory, or permissive setting is a lead. A strong report connects an eligible released version, attacker-controlled input, reachable behavior, missing control, and observable impact. Audit the application and the machinery that builds and distributes it as two connected attack surfaces.
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.
CI and releases: trace untrusted input into privileged workAudit workflow events, shell interpolation, untrusted builds, artifact consumers, and release identity as one privilege chain.
Open canonical module and reverse references
- 01Identify event trust
- 02Map executable input
- 03Inspect privileges
- 04Follow artifacts
- 05Validate locally
- 06Verify release identity
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Draw the actual privilege chain#
For every release-capable workflow, record event actor, workflow-file revision, checked-out revision, executable inputs, token permissions, reachable secrets, runner persistence, and final consumer. Distinguish data review from code execution: a checked-out contribution becomes executable through Makefiles, package install hooks, test discovery, plugins, and local actions as well as explicit shell scripts.
Use one row per job. Include reusable workflows and composite actions; apparent isolation at the top-level YAML may disappear inside their implementation. A permissive token setting without a lower-trust entry is a hardening observation, not a completed takeover claim.
| Boundary | Local evidence to collect | Comparison control |
|---|---|---|
| Metadata → generated shell | Original expression and resulting script | Environment variable passed as data |
| Fork content → privileged build | Exact fetched revision and executed file | Trusted base revision only |
| Test artifact → publisher | Producer identity, run association, consumed path | Artifact from expected trusted build |
| Build → attestation → release | Artifact digest and accepted signer identity | Wrong-source artifact rejected |
Triage commands and a safe coding pattern#
These examples inspect local files; they have not been run against a target. Grep-style matching deliberately overselects candidates.
rg -n --hidden -g '*.yml' -g '*.yaml' \
'pull_request_target|workflow_run|issue_comment|permissions:|id-token:' \
./repo/.github
rg -n --hidden -g '*.yml' -g '*.yaml' \
'download-artifact|upload-artifact|cache-mode|head\.sha|npm install|make ' \
./repo/.github
Passing untrusted metadata through an environment variable avoids inserting its contents into the generated shell program. Quote it and do not subsequently eval it. This pattern does not solve dangerous argument semantics in downstream tools. GitHub secure use.
- name: Inspect title as data
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: printf '%s\n' "$PR_TITLE"
Check current protections before declaring a pwn request#
GitHub currently documents fork-ref checkout protection for pull_request_target and read-only default cache access for that event. Inspect action revision, explicit allow-unsafe-pr-checkout, cache-mode, manual fetches, repository policy, and actual execution. The trigger name alone proves neither execution nor writable cache. Controls can differ from older published examples. Current pull_request_target guidance.
Hypothetical trace: an unprivileged test job uploads report.zip; a privileged follow-up selects an artifact by a generic name; an extracted helper is executed before publication. Validate run/repository binding, extraction destination, and consumer behavior in your own fixture with an inert marker and dummy credentials. A successful workflow conclusion does not authenticate every byte it produced. An artifact parsed strictly as data may break the supposed execution chain. GitHub Security Lab workflow analysis.
Follow identity to the shipped artifact#
For a known public artifact, this read-only verification example contacts GitHub. Replace repository, workflow, and commit with expected identities established independently; do not copy identities out of the untrusted artifact and then “verify” against them.
gh attestation verify ./dist/product.tar.gz \
--repo OWNER/REPO \
--signer-workflow OWNER/REPO/.github/workflows/release.yml \
--source-digest EXPECTED_COMMIT \
--format json > ./evidence/attestation.json
A reusable workflow may be the signer rather than the calling workflow. Verified provenance supports an origin claim; it does not prove the builder was uncompromised or the software is vulnerability-free. GitHub CLI verification.
Trusted publishing reduces long-lived publish-token exposure, but a compromised authorized workflow can still obtain publishing authority. Review environment approval, workflow identity, build separation, and remaining legacy credentials. PyPI trusted publisher security model.
Report and retest gate#
- A permitted low-trust actor controls the demonstrated input.
- Exact workflow/action revisions and security defaults are captured.
- Inert local reproduction reaches the privileged consumer.
- Real permissions are supported by configuration, not guessed from a trigger.
- Artifact run binding and signer/source identity are checked.
- Patched data handling rejects the case while normal releases still work.
Report the reachable capability and its prerequisites. Do not run target publishing steps or use discovered tokens to prove a local workflow hypothesis; maintainers can confirm live permissions through a coordinated reproduction when needed.
For GitLab repositories, also use GitLab pipeline boundaries: parent-project merge-request execution, job-token permissions and runner isolation have distinct semantics.
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.
Libraries and parsers: bounded fuzzing to a minimal reproducerBuild a realistic local harness, instrument the target, bound fuzzing resources, and turn unique faults into defensible library or consumer reports.
Open canonical module and reverse references
- 01Choose contract
- 02Build harness
- 03Instrument target
- 04Fuzz within bounds
- 05Minimize root cause
- 06Verify consumer impact
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Choose an attacker-relevant parser contract#
Select a public API and a realistic input boundary: imported image, archive, document, packet, model file, project metadata, or CLI input file. Record who supplies bytes, the consumer's preprocessing, format limits, parser options, and the privilege of the process. Fuzzing an internal helper with impossible arguments may reveal a harness defect rather than a violated library contract.
Keep the first harness narrow: one parse operation, deterministic configuration, fresh state for each input, bounded allocations, and cleanup on both success and failure. Remove network and uncontrolled filesystem side effects. Seed with a few valid, small representative samples to reach deeper code; an all-random corpus may spend every iteration failing the magic-header check.
Harness and build cheat sheet#
The following is a template: product_parser.h, ProductDocument, and the parser functions are placeholders to replace with the target's documented API. Return/error ownership must follow that API. The commands have not been executed here.
#include <cstddef>
#include <cstdint>
#include "product_parser.h"
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
if (size > 65536) return 0;
ProductDocument* doc = product_parse(data, size);
if (doc != nullptr) product_document_free(doc);
return 0;
}
Build the target library itself with compatible sanitizer and coverage instrumentation; instrumenting only the harness misses much of the parser's behavior. Replace the source list with its reviewed build recipe. The example assumes Clang and a compatible C++ project. AddressSanitizer build guidance.
clang++ -g -O1 -fno-omit-frame-pointer \
-fsanitize=fuzzer,address,undefined -fno-sanitize-recover=undefined \
./fuzz_parser.cc ./parser.cc -o ./fuzz_parser
mkdir -p ./corpus ./artifacts ./evidence
./fuzz_parser ./corpus -max_total_time=120 -timeout=2 \
-rss_limit_mb=512 -max_len=65536 -artifact_prefix=./artifacts/
Add reviewed seed files before running. libFuzzer's time, input-length, timeout, and RSS settings bound the campaign; they are not a VM resource boundary. Run inside your local disposable environment with external memory/CPU limits appropriate for the workload. Track new coverage and distinct root causes rather than raw executions. libFuzzer reference.
Interpret the failure precisely#
| Observation | Immediate question | Defensible first claim |
|---|---|---|
| ASan heap read overflow | Does the public API permit this byte sequence? | Out-of-bounds read in the tested build |
| ASan write overflow | Which length check and allocation disagree? | Out-of-bounds write; execution impact unproven |
| Use-after-free | Did the harness violate ownership rules? | Lifetime defect if public usage is valid |
| UBSan arithmetic finding | Does overflow reach an unsafe allocation/index? | Undefined operation plus traced consequence |
| Timeout or RSS limit | Is it reproducible at modest input sizes? | Bounded evidence of resource exhaustion |
-fno-sanitize-recover=undefined makes the selected UBSan failures terminate instead of allowing a fuzzing run to continue after a diagnostic, which helps preserve reproducible failing inputs. UBSan checks selected undefined behaviors, not every arithmetic error. Unsigned integer wrap is defined in C/C++ and is not part of the default undefined group; choose additional checks when the target's threat model requires them. UndefinedBehaviorSanitizer.
Minimize and verify the consumer#
Copy one candidate into ./artifacts/repro.bin, then minimize with a unique output path. Preserve the original input, toolchain, build flags, revision, and symbolized trace.
./fuzz_parser -timeout=2 -rss_limit_mb=512 ./artifacts/repro.bin
./fuzz_parser -minimize_crash=1 -max_total_time=60 -timeout=2 -rss_limit_mb=512 \
-exact_artifact_path=./artifacts/minimized.bin ./artifacts/repro.bin
Replay the minimized bytes through a supported consumer in the pinned OSS lab. If the consumer rejects them before calling the library, report the library contract separately and leave consumer exposure unproven. A sanitizer crash is not proof of remote code execution, and its absence in an optimized build does not erase a confirmed memory-safety defect.
Hypothetical root cause: a header count is multiplied into a narrow allocation size, then a wider loop trusts the original count. Preserve the values, allocation site, failing access, and missing bound. The fix should bind accepted count to available input and checked allocation size; merely suppressing the crash is insufficient.
- Public API and ownership contract respected.
- Target code, not only harness, instrumented.
- Resources, seeds, revision, and toolchain recorded.
- Minimal input reproduces the same root cause.
- Normal input succeeds in affected and fixed builds.
- Library impact and real consumer exposure are stated separately.
Replay timeout and memory artifacts under the same resource limits as discovery. A minimized crash must retain the original root cause; a shorter input that fails differently is not equivalent evidence.
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.
Container and Kubernetes isolationTest RBAC, workload identity, admission, networking and host exposure as separate enforcement layers.
Open canonical module and reverse references
- 01Pin lab and isolation promise
- 02Map permission layers
- 03Test canary access
- 04Compare admission and network controls
- 05Remove only recorded fixtures
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and invariant#
Use an isolated cluster or an explicitly provided test namespace with two tenant identities. Record context, API version, namespace, CNI, admission configuration, image digest and service accounts. Prepare one canary Secret and one inert HTTP service using administrator fixtures. A tenant may not acquire another tenant's data, workload identity or host authority through the deployment features it is allowed to use. A namespace name alone does not specify the isolation promise.
Procedure#
Start from the actual low-privilege identity. Use its own kubeconfig context. Separate resource reads, workload creation, role binding, impersonation and token issuance. Review permission grants before using them.
kubectl auth can-iasks the authorization layer; it does not prove admission acceptance or exploitation. RBAC risks, permission query referenceCompare direct and indirect canary access. The fixture owner reads its canary; the restricted principal should be denied. If workload creation is permitted, inspect whether a pod can choose a more powerful service account or mount a forbidden namespace resource. In the lab, use a one-shot workload that checks only the synthetic marker and exits. Record its resulting service account and generated pod, not only the submitted template. Service-account identity and its authorization are separate configuration elements. Service accounts
Exercise admission coverage. Use an allowed manifest as a positive control. Compare one owner-approved manifest that violates the advertised policy, then the equivalent supported update path. In a disposable cluster, compare admission responses and final persisted objects. Avoid actual privileged workloads when admission rejection already answers the test. Server dry-run still contacts admission components; it is not offline inspection. Admission webhook guidance
Measure network isolation independently. From two pre-created test pods, request the same tiny canary endpoint once. Record source/destination labels, namespaces, port and policy direction. The allowed source should receive the marker; the disallowed source should not. A timeout is inconclusive until the service and allowed route work. Policy objects require an enforcing network implementation; do not infer isolation from YAML alone. NetworkPolicy behavior
Inspect the container-to-host boundary. Record privileged mode, capabilities, host namespaces, host paths, socket mounts and UID mapping. Root inside a container does not establish host root. Where the owner supplies an isolated host fixture, a read of a synthetic host-mounted marker can demonstrate unintended filesystem exposure without reading host credentials. Compare with the restricted deployment. Pod Security Standards, Docker security
Command card#
These commands query one named resource or inspect one owned container. They do not create workloads.
kubectl --context lab-tenant-a --request-timeout=10s \
-n tenant-b auth can-i get secrets/bounty-canary
kubectl --context lab-tenant-a --request-timeout=10s \
-n tenant-a auth can-i create pods
kubectl --context lab-owner --request-timeout=10s \
-n tenant-a get pod bounty-probe \
-o jsonpath='{.spec.serviceAccountName}{"\n"}'
docker --context lab-engine container inspect bounty-probe \
--format '{{json .HostConfig.Privileged}} {{json .HostConfig.Binds}}'
Expect no for cross-tenant Secret access. A yes is an authorization finding candidate; verify only the supplied canary when reading is allowed. The inspect output establishes configuration, not an escape exploit. Docker inspect
Evidence, cleanup and checklist#
Store the starting role, requested object, admission decision, resulting workload, canary result and comparison case. Link business-level ownership to the shared authorization matrix. False positives include intended cluster administrators, intentionally shared namespaces and policies ineffective only because the lab omitted the production CNI. Describe those limitations explicitly.
- Namespace, workload, node and network boundaries separated.
- Low-privilege context used for reproduction.
- Only synthetic Secrets and markers accessed.
- Admission acceptance distinguished from runtime impact.
- Saved fixture UIDs checked before deletion.
- Disposable pods, role bindings and markers removed without deleting shared namespaces.
Infrastructure configuration: from repository default to deployed impactTrace IaC, charts, Compose and image packaging into supported deployments, including exposed services and sensitive artifacts.
Open canonical module and reverse references
- 01Freeze supported configuration
- 02Render with synthetic values
- 03Trace risky settings
- 04Compare deployed controls
- 05Remove artifacts and resources
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and invariant#
Choose one supported deployment path: Terraform workspace, Helm chart, Docker Compose stack or image build. Pin the repository commit, lockfiles, provider/chart versions, image digest and documented values. Use an isolated environment with synthetic credentials and no workstation cloud credentials. A supported configuration must preserve its documented trust boundaries and keep deployment secrets away from unintended readers. A linter warning is a hypothesis until the affected deployment and boundary are established.
Procedure#
Reconstruct the real defaults. Read quick-start instructions, production overrides, environment-variable precedence and optional features. Record which configuration a normal user actually follows. Compare the default deployment with the maintainer-recommended secure profile. A development-only example is not automatically a production vulnerability.
Render trusted configuration. Render supplied charts or Compose files with synthetic values, then inspect resulting listeners, host publications, identities, volumes and access policies. Keep rendered files private: interpolation can materialize credentials. Rendering is useful evidence of intended resources, but it does not prove deployment reachability. Helm template, Compose configuration
Trace secret lifetime. Follow a synthetic marker from input variable through plan, state, output, image layer, artifact archive and logs. Ask which principal can read each artifact. Terraform's
sensitivedesignation mainly controls display; state or JSON output can still contain values. Prefer a known fixture plan over reading live state. Do not run unreviewed initialization, provider binaries or provisioning hooks on a credentialed workstation. Terraform sensitive data, Terraform showInspect packaging separately from runtime. For an owned image containing synthetic inputs, review build history and layers against the build recipe. A secret removed from a later filesystem layer may still exist in earlier content. Check whether build arguments or environment variables carry credentials where a build-secret mechanism is appropriate. Record the exact image digest and artifact accessibility; a random string match does not establish a live credential. Docker build secrets
Confirm one deployed consequence. In the disposable deployment, test one known canary from the intended allowed location and an owner-controlled disallowed location. For exposed databases, reuse network-service boundaries. For broad roles or storage policies, reuse cloud IAM and storage delivery. Change only the relevant setting, redeploy the lab fixture and repeat the pair.
Command card#
Use a reviewed, already initialized Terraform fixture and trusted chart. These examples write only local evidence; commands are not instructions to initialize an arbitrary repository.
umask 077
helm template bounty ./trusted-chart \
--values ./fixture-values.yaml > ./fixture-rendered.yaml
terraform -chdir=./lab-iac show -json ./fixture.tfplan \
> ./fixture-plan.json
docker image history OWNED_SYNTHETIC_IMAGE \
--no-trunc --format '{{.CreatedBy}}' \
> ./fixture-history.txt
rg --files-with-matches --fixed-strings \
'BB_SYNTHETIC_SECRET_2026' \
./fixture-plan.json ./fixture-history.txt ./fixture-rendered.yaml
A returned filename proves only that the chosen marker is present in that file. The security result also needs unintended artifact access and the intended confidentiality rule. Restrict provider execution to the disposable lab even for inspection commands that may need installed plugins. Image history reference
Interpretation, cleanup and checklist#
The report should connect supported input → rendered setting → deployed resource → unintended reader or actor → canary result. Include the secure comparison and minimum affected configuration. Avoid claiming that every deployment is exposed when a particular override is required. Public ports without usable privilege, fake example secrets, intentional debug builds and unreachable rules are common false positives.
- Supported mode and exact dependency versions pinned.
- Default values separated from operator overrides.
- Synthetic data used throughout artifacts.
- Actual artifact reader or service boundary identified.
- Remediated comparison reproduces the expected denial.
- Only fixture resource IDs destroyed; volumes, plans, logs and disposable credentials cleaned up.
Database, cache, queue and management service boundariesMove from a reachable socket to a minimal proof of unintended service authority using synthetic records and paired identities.
Open canonical module and reverse references
- 01Record exact endpoint and allowed method
- 02Establish authenticated baseline
- 03Read one forbidden fixture
- 04Compare identity and network controls
- 05Document and remove canaries
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and invariant#
Record one service row: owner, exact IPv4 or IPv6 address, hostname, transport, port, product build, authentication method and intended access boundary. A hostname in web scope does not automatically include every backing service. Prepare a restricted account, an allowed account and one synthetic key, row, queue or management fixture. A network position or low-privilege account must not gain the protected data or administrative operation outside its stated permissions.
Use version banners only to select hypotheses. Vendor backports and disabled features can invalidate a banner-to-CVE match. First establish a normal connection, intended authenticated operation and expected rejection. Keep source network and timestamps constant when comparing identities.
Procedure#
Separate reachability from authority. Record a connection outcome without treating an open port as a vulnerability. Check whether the service is intentionally public, behind mutual TLS or protected by native authentication. A health response is not administrative access.
Use one exact resource. For Redis, request only the agreed canary key; do not run keyspace enumeration, configuration changes or flush commands. Compare anonymous access, the permitted principal and the restricted principal. Redis network protection, authentication, ACLs and TLS solve different parts of this boundary. Redis security
Check connection rules and database grants independently. In PostgreSQL, examine the owner-supplied host-based authentication rule selected by connection type, source, database and user. Establish whether authentication genuinely happened, then test permission for one fixture row. A trusted local administration connection is not evidence of internet exposure. PostgreSQL host authentication
Model queues as stateful resources. In an isolated broker, create two fixture virtual hosts and named queues. Compare whether A can declare, publish to or consume B's fixture according to the contract. Use a single marked message and account for acknowledgement behavior; consuming production messages changes application state. Broker login does not imply permissions across all virtual hosts. RabbitMQ access control
Evaluate management APIs and network parity. For an owned Docker daemon, review listener and client authentication configuration; use only an approved metadata endpoint for the initial proof. Do not create privileged containers to confirm broad authority on production. Compare an explicitly owned IPv4 and IPv6 service only when both are in scope: different exposure is relevant when it defeats the same promised restriction. Docker daemon protection
Command card#
The Redis endpoint below is an isolated local fixture. The environment removal prevents an inherited password from contaminating the unauthenticated control. The database example uses the supplied test account and private password file, with TLS identity verification and a read-only transaction.
env -u REDISCLI_AUTH timeout 10 redis-cli \
-h 127.0.0.1 -p 16379 --raw GET bounty:canary
timeout 15 psql -X -w \
"host=db.fixture.example port=5432 dbname=bounty_fixture user=lab_low sslmode=verify-full sslrootcert=./fixture-ca.pem connect_timeout=5 options='-c statement_timeout=3000'" \
-v ON_ERROR_STOP=1 \
-c "BEGIN READ ONLY; SELECT marker FROM bounty_canary WHERE id=1; ROLLBACK;"
An unauthenticated marker demonstrates read access to that fixture. NOAUTH, NOPERM, connection failure and an absent value mean different things; compare the known-good control. For PostgreSQL, distinguish TLS failure, authentication failure and SQL permission denial. Credentials can come from a password file even with -w; that flag disables prompting, not password authentication. Redis CLI, PostgreSQL connection parameters, psql options
Evidence, false positives and cleanup#
Save the endpoint, source, verified account, precise operation, response, fixture ownership and comparison. Public service banners, intentionally anonymous health checks and intended shared queues are not equivalent to data exposure. Reuse authorization matrices for per-resource checks and identity/session testing for revocation behavior.
- Allowed and denied controls have working connectivity.
- No wildcard reads, real queue consumption or configuration mutation.
- Network access distinguished from authentication and authorization.
- Lab result separated from unverified production applicability.
- Canary records, messages and test accounts removed using their recorded identifiers.
AI tools: input trust, execution identity and approvalsProve when lower-trust content changes a protected tool action, then locate the missing authorization or consent boundary.
Open canonical module and reverse references
- 01Pin legitimate task
- 02Map context to tool
- 03Run paired trials
- 04Check server decision
- 05Minimize evidence
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and security invariant#
Start with a local agent application, two researcher-owned identities, and stub tools that write only to an in-memory ledger or local JSONL file. Record the model identifier, sampling settings, application commit, available tools, connector scopes and legitimate user task. The invariant is: retrieved content can supply task data, but cannot grant permission, change the acting principal or silently expand an approved operation. Reuse the authorization matrix for tool objects and identity/session procedure for credentials.
Make a four-column trust map: attacker-editable source, transformation, tool argument, server-side decision. Include tool descriptions, tool results, document text, OCR and summaries. Follow the actual source path using source-to-sink review. A changed answer style demonstrates instruction influence; a security finding needs an unauthorized read, write, destination, privilege or other protected consequence. OWASP separates excessive tool functionality, permissions and autonomy. OWASP Excessive Agency
Controlled procedure#
- Record the baseline. Ask the owned victim agent to summarize one benign shared document. Save the exact prompt, document hash, tool calls and outbox state. Verify that a separately authorized mock write succeeds and an unapproved write is rejected; otherwise a silent tool failure could masquerade as protection.
- Change one lower-trust input. In a copy of the document, add a test instruction requesting a harmless change to the stub destination. Keep task, tools and identities constant. Run five fresh baseline trials and five fresh candidate trials with a fixed small token budget. Save every attempt, including failures; do not assume sampling controls make a hosted model deterministic.
- Inspect execution independently. Compare suggested calls, dispatched calls and committed stub events. Record which principal reached the backend. A model claiming success is insufficient. Repeat the same operation directly through the local tool adapter to determine whether the backend itself has an ordinary object-authorization defect.
- Bind approval to the action. Approve one harmless draft, then vary only its object ID, destination or content in the local fixture. The approved values and executed values must match, or the product must request renewed approval according to its contract. Compare canonical argument digests, not UI labels.
- Check the MCP boundary separately. With your own OAuth fixtures, compare a correct-resource token with a valid token for another resource. Exercise client A consent followed by a fresh client B request. MCP requires intended-audience validation and per-client consent in the documented proxy scenario; inherited upstream consent is not sufficient. Match all requests to the deployed protocol version. MCP authorization, MCP security guidance
Command and evidence cheat sheet#
Run these local inspection commands in the reviewed lab checkout. lab/tool-events.jsonl is your recorded adapter output, with one JSON object per event; these commands inspect evidence and do not invoke an agent.
rg -n 'approve|consent|tools/call|audience|tenant_id|workspace_id' ./src
python3 -I -S -m json.tool lab/case.json
python3 -I -S - <<'PY'
import json
from collections import Counter
with open('lab/tool-events.jsonl') as stream:
rows = [json.loads(line) for line in stream if line.strip()]
print(Counter((r['case'], r['decision']) for r in rows))
PY
Use event fields case, trial, principal, tool, argument_digest, decision and committed. Redact credentials. Python's JSON CLI validates the case file; it does not assess the security result. Python JSON documentation
Evidence, false positives and completion#
Preserve the legitimate task, attacker capability, differing document bytes, full relevant tool trace, principal and controlled side effect. Exclude hallucinated actions, canaries already present in the attack text, intentionally authorized tool use, and tests requiring nonexistent attacker editing rights.
- Baseline allowed operation and denied operation both work as expected.
- Every candidate has an unchanged benign comparison.
- Suggested, attempted and committed actions are distinguished.
- Approval arguments and execution identity are recorded.
- Successes/attempts and cleanup of local fixtures are included.
Continue across task lifetime#
An initial allowed or denied tool call does not cover later resumed work. Continue with AI state lifecycle for memory/checkpoint ownership, MRTR state binding, cancel/retry behavior and private caches. Apply resource budgets to tool fan-out, loop limits and per-principal cost without generating load on a live service. For model-output HTML, SQL or file effects, inspect the final consumer using browser trust, injection tracing and file boundaries; model output is input to those components.
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.
Contract state: authorization, callbacks and accountingTurn protocol promises into explicit invariants and test realistic local operation sequences with independent accounting.
Open canonical module and reverse references
- 01Resolve implementation
- 02Write unit-aware invariants
- 03Build actor baseline
- 04Exercise sequences
- 05Minimize state trace
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and security invariant#
Prepare a reviewed local checkout with dependencies and compiler already available. Use a private local EVM test harness; keep wallet secrets absent and review configuration for FFI, RPC endpoints and fork creation before execution. Record commit, compiler settings, proxy family, implementation, initialized roles and supported token behavior. A local reproduction against a simplified model explains an idea; an eligible report must establish that the same path exists in the relevant deployed implementation.
Write properties using explicit units: assets, shares, debt, fees and time. For example, redeeming a claim cannot pay more assets than its defined entitlement, and an ordinary actor cannot change the implementation or initialize an already initialized proxy. Do not equate token balance and share supply. Reuse the authorization matrix for actors and protected transitions. OpenZeppelin documents proxy-specific initialization and upgrade behavior, so inspect the actual family and version. OpenZeppelin proxies
Controlled procedure#
- Build realistic actors. Separate setup administrator, ordinary depositor A, depositor B and callback receiver. Setup may allocate synthetic tokens, but record each privilege and stop using administrator impersonation before the candidate sequence. Verify a normal deposit/redemption round trip and expected privileged-operation rejection.
- Trace callbacks. Mark every external call and the state visible at that instant. Give an owned callback contract one bounded reentry attempt into the original or a related function. Compare with a receiver that accepts the callback without reentry. Solidity warns that reentrancy concerns external contract calls and interacting contracts, not only Ether transfers. Solidity security considerations
- Track independent accounting. Record user assets, shares, protocol holdings, debts and fees before and after every action. Calculate the expected relationship independently of the implementation under test. For rounding, test zero supply, minimal amounts, mixed decimals, partial redemption and a direct donation, using only supported tokens. Quantify attacker cost as well as victim loss. ERC-4626 conversions and donations can expose rounding-sensitive states. OpenZeppelin ERC-4626
- Exercise ordering. Try short valid sequences: deposit → transfer shares → redeem; deposit → donate → second deposit; callback → related read/write; initialize → initialize again. Change one amount, role or order at a time. Count successful state-changing calls so an all-revert fuzz run cannot be mistaken for coverage.
- Minimize the failure. Reduce the sequence and amounts while preserving the violated property. Run the identical sequence with the benign callback or corrected authorization as a control. Preserve the earliest inconsistent state as well as the final balance difference.
Harness and command cheat sheet#
Create target-specific test functions; the names below are a suggested harness contract, not built-in Foundry scans.
| Function | Required assertion |
|---|---|
test_BaselineRoundTrip |
Expected fees and rounding explain final balances |
test_UnauthorizedUpgradeDenied |
Ordinary actor cannot alter implementation |
test_CallbackInvariant |
Bounded callback preserves the chosen property |
test_MinimizedSequence |
Replays exact candidate and checks measured effect |
git rev-parse HEAD
forge --version
rg -n 'ffi|rpc_endpoints|createFork|createSelectFork' foundry.toml test
forge test --match-test '^test_BaselineRoundTrip$' -vvvv
forge test --match-test '^test_CallbackInvariant$' -vvvv
forge test --match-test '^test_MinimizedSequence$' -vvvv
Run only after the named local tests exist and their setup has been reviewed. --match-test selects your tests; verbosity exposes local traces. These commands do not broadcast transactions, but test code can explicitly access external resources, which is why harness review matters. Foundry test reference
Evidence, false positives and completion#
Provide the implementation identity, exact harness, trace, initial capital, privileges, unit-aware balance sheet and bounded impact. Exclude unsupported malicious-token assumptions, fabricated oracle authority, admin-only behavior working as designed, and a callback that cannot occur in the real path.
- Baseline exercises the same implementation and setup.
- Actor powers match the claimed attack model.
- Successful transitions and reverts are counted.
- Loss is demonstrated independently of total value locked.
- Reproduction stays entirely in the local EVM.
Contract signatures and oracles: context, freshness and unitsTest signed-action context and price-consumer assumptions with local keys, mock feeds and explicit negative controls.
Open canonical module and reverse references
- 01Record trust contract
- 02Prove valid baseline
- 03Vary one binding
- 04Vary feed metadata
- 05Calculate consequence
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and invariants#
Create two locally deployed verifier instances, a local-only signer, two ordinary actors and a mock feed whose state you can vary. Write the expected signed action as a tuple: signer, action type, recipient, amount, nonce, deadline, chain and verifying contract. Record which fields the application intentionally binds. The invariant is: a signed instruction permits only its intended action and replay policy. EIP-712 defines typed hashing and domain separation; replay protection remains the application's responsibility. EIP-712
For price consumers, record base/quote currency, feed decimals, token decimals, supported price range, freshness policy and dependency-failure behavior. The invariant is: an accepted observation has the meaning and validity required for the resulting economic decision. Chainlink exposes round metadata and timestamps; consumers must interpret them in their own integration. Chainlink data-feed API
Controlled procedure#
- Prove both baselines. A valid local signature executes its harmless ledger action once. A fresh positive-price feed produces the expected collateral or quote calculation. Verify an invalid signature and an explicitly disallowed stale observation are rejected, where that policy is part of the product's contract.
- Vary one signature dimension. After the valid action, replay exactly the same bytes. Then use fresh baselines to vary only the recipient, amount, action type, verifier instance, chain context or expired timestamp. Record the recovered signer and nonce-consumption point. A reverting transaction normally rolls back its state; prove whether the replay state survives a successful action rather than assuming a pre-revert write consumed the nonce.
- Check alternate signer paths. If contract wallets are supported, compare an owned wallet returning the ERC-1271 success value
0x1626ba7e, a wallet returning a different value, and a reverting wallet. A non-reverting call alone does not establish a valid signature. ERC-1271 validity can depend on current contract state: change the authorized local signer after a valid baseline and repeat only where the consumer promises current authorization. Record validation at execution versus an intentionally earlier irrevocable authorization. Do not classify unsupported signature formats as a vulnerability. Reuse identity and session binding for off-chain issuance. ERC-1271. - Vary feed metadata. Starting from a valid response, test an old timestamp, zero or negative answer where the feed type can produce it, changed decimals, failed feed call and mismatched quote currency. Keep all other fields realistic. Demonstrate the consumer's accepted result and economic effect; an artificial impossible feed response is a robustness result until reachable conditions are shown.
- Test L2 recovery if applicable. Model sequencer down, just restarted and beyond the configured grace period separately. This is integration-specific: not every chain or feed uses identical checks. Chainlink's L2 guidance explains uptime signals and post-recovery grace periods. L2 sequencer feeds
- Calculate the consequence. Compare collateral, debt, quote or liquidation outcome with an independently calculated expectation. Include price units, rounding, capital, timing, fees and user constraints. A missing named check without a reachable incorrect decision remains a review lead.
Local cheat sheet and case matrix#
Implement these names in your reviewed local Foundry harness; they are not scanner features. The signer key must be generated or hard-coded solely for this disposable local fixture.
rg -n 'ecrecover|nonces|deadline|DOMAIN_SEPARATOR|latestRoundData|decimals' src
forge test --match-test '^test_ValidSignature$' -vvvv
forge test --match-test '^test_ReplayRejected$' -vvvv
forge test --match-test '^test_WrongDomainRejected$' -vvvv
forge test --match-test '^test_StaleFeedRejected$' -vvvv
forge test --match-test '^test_PriceUnitInvariant$' -vvvv
--match-test selects target-specific tests and trace verbosity helps identify the accepted transition. Use preinstalled compiler/dependencies and no RPC/fork/FFI setup for these local fixtures. Foundry test reference
| Comparison | Hold constant | Vary |
|---|---|---|
| Replay | Signature, actor, verifier | Prior successful consumption |
| Domain | Message and signature | Local verifier or chain context |
| Freshness | Price and units | Observation timestamp |
| Unit conversion | Economic value | Supported decimal representation |
Evidence and checklist#
- Exact message schema, domain and successful baseline are saved.
- Only one context field changes in each negative case.
- Mock feed deviations are separated from reachable attacker powers.
- Nonces, timestamps, units and downstream decisions appear in evidence.
- No real signatures, transactions or funds are used.
Firmware update and boot trust: signed bytes, product binding and rollbackSeparate download trust, update authorization, image verification, rollback policy and boot enforcement using local artifacts.
Open canonical module and reverse references
- 01Identify image and board
- 02Trace acceptance gates
- 03Compare artifact variants
- 04Inspect boot and recovery
- 05Record bounded result
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Setup and security invariant#
Obtain an authentic image and record product/SKU, board revision, SoC revision, firmware version, source and SHA-256. Use an isolated non-root workspace with no device attached for initial review. Prepare an original and disposable copies, the relevant verification code if available, and a recoverable owned lab device only if later needed. The invariant is: only authorized, correctly targeted software at an allowed security version reaches execution.
Separate five gates: who can request an update, how metadata is authenticated, which payload bytes are verified, whether that image fits this product, and what the boot path enforces. TLS does not replace image authenticity. An encrypted package is not automatically signed. OWASP treats update authorization, signature verification and rollback as distinct tests. OWASP firmware update tests
Controlled procedure#
- Map the container. Identify headers, manifest, payload, signatures and any bootloader/recovery component. Locate verification before extraction, writing and execution. If source is available, follow length and offset calculations using source dataflow. Keep extraction confined because the archive parser is itself exposed to untrusted input.
- Establish the acceptance baseline. Feed an unchanged vendor image into the actual local verifier, emulator or documented validation-only path. Confirm it accepts the correct product and rejects a genuinely malformed sample. A checker that rejects every file provides no useful security comparison.
- Change one field or byte. On disposable copies, alter one non-executable payload marker, remove the signature, change a target identifier or alter a signed manifest field. Record both package parsing and signature decisions. Use a vendor-accurate container parser: an arbitrary byte flip that destroys compression only demonstrates malformed-input rejection.
- Test version policy. Compare an allowed current image with an authentic older signed image that has a known lower security version. Inspect whether the permitted caller can lower the stored security counter or select an alternate recovery path. A lower marketing version is not necessarily a lower security version. Espressif's OTA documentation distinguishes application rollback after failed validation from anti-rollback based on security version. Espressif OTA
- Inspect execution trust. Map ROM → bootloader → selected application, including alternate slots and recovery. Determine whether an update accepted into storage would still fail at boot. For supported ESP32 revisions, Secure Boot v2 verifies bootloader and application signatures; applicability depends on chip revision and enabled configuration. Espressif Secure Boot v2
Local inspection cheat sheet#
mkdir -p evidence
sha256sum firmware.bin > evidence/firmware.sha256
file firmware.bin > evidence/firmware-format.txt
binwalk firmware.bin > evidence/firmware-layout.txt
rg -n -i 'signature|rollback|secure_version|board_id|verify' ./source
Binwalk's default file argument identifies embedded content; extraction requires a format-appropriate, separately reviewed procedure. These commands do not flash, boot or rewrite firmware. Binwalk README
Create this reusable acceptance matrix before modifying any copy:
| Variant | Expected verifier result | Record separately |
|---|---|---|
| Authentic current image | Accept for correct product | Parser, signature, model and version |
| Altered protected payload | Reject | Verified byte range and error stage |
| Wrong product | Reject if product binding is required | Target identifier and compatibility |
| Older security version | Enforce documented downgrade policy | Counter and recovery behavior |
Evidence, false positives and completion#
Report precisely where the candidate crosses a boundary: parse acceptance, staging, installation or actual execution. Do not turn acceptance by a standalone inspection tool into a device execution claim. Debug builds, an administrator's explicitly supported downgrade, unsigned non-executable padding and a vendor checksum mismatch alone are common false positives.
- Authentic baseline and every modified copy are hashed.
- Parser rejection and authentication rejection are distinguished.
- Security-version policy and attacker update privilege are stated.
- Boot/recovery enforcement limits the claimed impact appropriately.
- No fuse, bootloader or production-device write was needed for this procedure.
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.
GitLab pipelines: contribution, job token and runner boundariesTrace GitLab execution context, included configuration, downstream inputs, job-token permissions and runner reuse using private synthetic fixtures.
Open canonical module and reverse references
- 01Pin version and pipeline context
- 02Resolve included configuration
- 03Map job and downstream authority
- 04Compare one private fixture
- 05Reset runner and artifacts
At each step: retain the control → record the observation → continue only while the next boundary is understood.
Scope and fixture contract#
Use an owned GitLab project pair and disposable runner, or owner-supplied configuration exports. Pin GitLab and Runner versions, feature flags, repository commit, pipeline ID, source project, target project and triggering user. Create a private synthetic artifact in project B and one job in A. Control over A's contribution or job must not acquire B's private authority or a release identity beyond the stated integration. Reuse CI and release tracing for artifact provenance and source data flow for executable input.
1. Establish which pipeline really runs#
Trace merge-request source, configuration revision, execution project and trigger actor independently. Fork pipelines ordinarily run with fork resources. A parent member can run a fork merge-request pipeline in the parent context, using fork-controlled configuration with parent resources and that member's permissions. Inspect the exact settings and approval path. Protected variables and runners have additional requirements, including same-project protected branches; do not infer their exposure merely from parent execution. Re-running an older pipeline can retain its original context. GitLab merge request pipelines
Build one row per job: who edits the YAML, who changes included files, which commit is fetched, which scripts execute, and what the resulting identity can do. Package installation, test discovery and generated scripts are executable inputs even when the visible job looks like a harmless test command.
2. Follow configuration and downstream trust#
Resolve include, extends, component inputs, dynamic child configuration and multi-project triggers before judging a job. Capture the merged configuration through an owner-provided export. For remote includes, compare a pinned expected file with a changed inert fixture; supported include:integrity verifies a SHA-256 digest and rejects a mismatch. A mutable reference is a candidate until the lower-trust editor and privileged consumer are connected. GitLab YAML reference
Track forwarded variables and generated child YAML into the receiving pipeline. A downstream job may interpret a value as a shell command, artifact selector, deployment environment or cloud role. Compare the allowed value and one disallowed synthetic value at that exact sink. Preserve the downstream pipeline's own policy and actor, since upstream success does not establish downstream entitlement. Downstream pipelines
3. Separate token restrictions from secret masking#
A job token is limited by its API capabilities and the triggering user's authority. Review the receiving project's allowlist, feature visibility and any fine-grained restrictions together. Allowlisting a project does not independently grant its users new project permissions. Distinguish currently running jobs from expired-token attempts. Review repository-push settings for the deployed GitLab version rather than assuming every job token is permanently read only. GitLab job tokens
Masked and hidden variables address exposure in interfaces; code already authorized to consume a variable can still misuse it. With a dummy value, identify the exact job receiving it, environment scope and later executable consumer. Never print real CI variables to inventory privileges. GitLab CI variables
4. Reproduce one boundary using inert controls#
| Candidate | Controlled comparison | Required evidence |
|---|---|---|
| Fork code enters parent authority | Trusted base versus inert fork fixture | Configuration SHA, execution project, actor and marker-only result |
| Job A reaches private project B | Allowed integration versus disallowed project/actor | Exact synthetic resource, token context and accepted/denied response |
| Runner leaks previous job state | Two owned jobs on a disposable runner | Known marker survives across a boundary that promises isolation |
| Publisher consumes wrong artifact | Expected producer versus wrong fixture run | Digest, selected run and actual trusted consumption |
For runners, inspect executor, host/socket mounts, shared working directories, cache policy and whether private images can be reused by unauthorized jobs. Use one predetermined synthetic marker path and one owned image. A shell executor or privileged container is a risk signal; the demonstrated issue is unintended cross-job access. Do not inspect other users' workspaces. Runner isolation guidance
Local command cheat sheet#
These searches read a local clone and execute no CI jobs. Follow results into referenced files; absence of a match is not coverage proof.
rg --files --hidden -g '!.git' -g '*gitlab*' -g '*.yml' -g '*.yaml' ./repo
rg -n --hidden -g '!.git' -g '*.yml' -g '*.yaml' \
'include:|extends:|trigger:|forward:|id_tokens:|environment:' ./repo
rg -n --hidden -g '!.git' \
'CI_JOB_TOKEN|CI_PIPELINE_SOURCE|CI_MERGE_REQUEST|privileged|pull_policy' ./repo
Evidence and completion#
- Pipeline project, configuration revision and triggering actor recorded separately.
- Parent/fork and protected-resource assumptions verified for the installed version.
- Included configuration, downstream inputs and executable consumers traced.
- Token capability, actor permission and receiving-project policy distinguished.
- Allowed and denied cases use the same private synthetic resource.
- Job logs contain no usable credentials; fixtures and runner state are removed.
A report should state the lowest actor that can initiate the chain, required maintainer interaction, exact acquired capability and secure comparison. Intended public artifacts, a rejected token, or deliberate shared runner trust do not establish a private boundary failure.
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 · Hard-coded product credentials · CI metadata interpreted as executable script · Untrusted contribution executed by privileged CI · CI artifact poisoning across a workflow trust boundary · Dependency confusion and unintended package origin · Unsafe object deserialization · JavaScript prototype pollution · Out-of-bounds memory read · Out-of-bounds memory write · Use after free in a public API or parser lifecycle · Integer overflow affecting allocation or bounds · Parser resource exhaustion and decompression amplification · Regular-expression denial of service · Predictable temporary files and unsafe creation · Untrusted executable, library, or module search path · Federated cloud role accepts the wrong workload identity · Anonymous access to private object storage · Unauthorized object creation or replacement · Signed storage capability exceeds its authorized scope · Kubernetes Secret access exceeds tenant permissions · Workload creation grants unintended service-account authority · Admission policy misses a supported object or update path · Container exposes unintended host files or privileges · Sensitive values exposed through state or plan artifacts · Secrets retained in container build artifacts · Supported deployment default exposes a protected service · Redis authentication or key ACL boundary failure · PostgreSQL trust authentication exceeds the approved network boundary · Message broker permissions cross a virtual-host boundary · Container daemon management endpoint lacks required client authentication · TLS peer certificate or hostname verification failure · Updater accepts an artifact outside its publisher trust policy · Indirect prompt injection into protected tool actions · Agent tool accepts unauthorized object or tenant parameters · Agent approval is not bound to executed arguments · MCP proxy consent confuses different clients · MCP accepts a token for another resource · RAG retrieval or citation resolver crosses tenant boundaries · RAG cache or fresh retrieval ignores revoked access · Contract callback reentrancy violates a state invariant · Contract initializer permits unauthorized ownership or configuration · Contract upgrade authority can be bypassed · Vault share conversion or rounding breaks entitlement · Signed contract action can be replayed unexpectedly · Signed action does not bind its intended context · Oracle freshness or dependency state permits an invalid decision · Oracle denomination or decimal conversion is incorrect · Firmware verifier accepts unauthenticated protected bytes · Firmware security-version state can be lowered · Firmware verification fails to enforce required product binding · Firmware contains a reusable shared authentication credential · Exposed repository or history secret · 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 · GitLab job token crosses an unintended project boundary · Shared CI runner exposes private state across jobs · Page message borrows unintended browser-extension authority · Delayed extension result reaches a different document · Agent conversation, checkpoint or memory crosses ownership boundaries · MCP resumed state changes authorization or request meaning · Private MCP result is reused across authorization contexts · Agent retry commits a protected single-use action twice · Untrusted model artifact reaches executable deserialization or custom code · Unbounded resource allocation or request amplification · Sensitive business flow exceeds its permitted frequency or allocation
Choose the correct research route#
| Route | Establish before investing | Useful deliverable |
|---|---|---|
| OSS product with a bounty | Exact repository, branches/releases, supported configurations, eligible vulnerability classes, disclosure channel, and current reward rules | Local reproduction on an eligible release with product-level impact |
| OSS project accepting disclosure | SECURITY.md, private reporting availability, supported versions, maintainer contact |
Coordinated vulnerability report; payment is not implied |
| Vendor-hosted service with public source | Whether the repository and live service are separately in scope; deployed-version uncertainty | Source-supported hypothesis, followed only by permitted service testing |
| Self-hosted deployment in an authorized program | Deployment owner, deployment-specific scope, edition, settings, plugins, and versions | Finding against that deployment with product/configuration attribution |
| Private source-assisted assessment | Permission to possess and process code, approved tooling/storage, branches, environments, third-party exclusions | Evidence retained within the agreed confidentiality boundary |
| Library, SDK, parser, CLI, or agent | Supported consumers, caller contract, attacker-controlled inputs, local versus remote impact | Minimal consumer/harness demonstrating the violated contract |
A public repository does not extend a program's scope to its maintainers' infrastructure, package registries, customers, or public deployments. A locally reproduced product vulnerability does not establish that every deployed instance is affected. If payment matters, capture the current program's eligibility terms before starting; a CVE and a bounty are separate outcomes. GitHub offers private vulnerability reporting only where maintainers enable it, and repository advisories support private discussion and fixes. GitHub: private reporting, repository advisories.
Staged playbook#
The following gates are recommended operating criteria, not platform-imposed requirements.
| Stage | Inputs and actions | Evidence | Exit gate |
|---|---|---|---|
| 1. Qualify | Read policy, supported versions, prior advisories, release notes, architecture, installation requirements | Scope snapshot, repository identity, likely audit themes | An eligible version and confidential reporting route exist |
| 2. Freeze | Preserve commit SHA, release tag, lockfiles, submodules, image digests, tool versions | Reproduction manifest and source inventory | Another researcher can identify the same inputs |
| 3. Build the lab | Review build scripts; install in a disposable VM; create synthetic users, tenants, data, and dependencies | Clean snapshot, config delta, health check, reset procedure | Normal workflows work and unintended egress is blocked |
| 4. Map | Trace entry points, authentication, authorization, queues, storage, integrations, and deployment boundaries | Data-flow map and role/resource matrix | Each priority feature has a known entry and protected operation |
| 5. Form hypotheses | Combine manual review, local scans, advisory review, and changed-code inspection | Ranked hypotheses with source paths and required assumptions | Each candidate states a falsifiable security invariant |
| 6. Validate | Exercise the smallest synthetic case; inspect call path; compare an authorized control | Sanitized requests, outputs, trace, before/after state | Impact reproduces without changing unrelated security controls |
| 7. Audit delivery | Review dependency, build, workflow, package, image, and IaC paths | Separate reachability and privilege chains | Supply-chain claims have a concrete consumer or privileged sink |
| 8. Report and retest | Minimize reproduction, bound versions, suggest fix, privately submit, retest patch | Report package and updated validation matrix | Recipient can reproduce; remaining assumptions are explicit |
Reproducible lab design#
Treat unfamiliar repositories and their dependency installation as executable input. Review Dockerfiles, Compose, Makefiles, lifecycle scripts, build backends, development-container settings, and CI scripts before invoking them. Use a disposable VM for untrusted builds; containers within it help reproduce services but are not a reason to expose the host Docker socket, SSH agent, cloud credentials, or personal home directory.
Separate the preparation phase, where reviewed dependencies and databases are downloaded, from the test phase. Bind application ports to loopback. Create synthetic tenant A and B, ordinary and privileged accounts, a local mail catcher, and local callback/file-storage services. Block external egress during exploit validation; replace integrations with controlled fixtures. Preserve a working baseline before modifying configuration.
Record environment differences explicitly: community versus enterprise edition, reverse proxy behavior, authentication mode, enabled plugins, database backend, OS/architecture, and documented deployment topology. An attack that depends on DEBUG, disabled authentication, or a deliberately exposed administrator interface has different prerequisites from one affecting the supported production setup. A dangerous default or documented deployment recipe can be a legitimate product issue; demonstrate the actual boundary and check program policy.
Application and source audit#
Start with business capabilities, then inspect implementation. OWASP emphasizes manual review for application logic, data flows, and contextual flaws that automated tools can miss. Use scanning to prioritize code; manually establish the security argument. OWASP: Secure Code Review.
For each feature, write one invariant: “a project member can retrieve documents only from projects they belong to,” or “an import cannot create files outside its assigned directory.” Trace both forward from attacker-controlled inputs and backward from sensitive operations. The intersection is your candidate path.
| Area | Trace precisely | Proof target |
|---|---|---|
| Authentication | Login, password reset, invitations, SSO callbacks, token issuance/refresh, revocation | A synthetic principal acquires or retains an unintended identity/session |
| Object/tenant authorization | Route → actor resolution → queryset/service → object → tenant/role check | Account A accesses a fixture owned by B through the real handler |
| Privileged actions | UI restriction versus server policy; API variants, bulk endpoints, background jobs | A lower role performs an operation reserved for a higher role |
| Injection | Input parsing → transformations → ORM/raw query, process, template, evaluator, interpreter | Input changes interpretation at the sink under supported conditions |
| Files and archives | Upload name/content → normalization → extraction → symlink handling → persistence → serving | A synthetic file crosses an intended path or content boundary |
| Outbound requests | URL parsing → scheme/host validation → DNS/redirect handling → fetch → response exposure | The application reaches a blocked local fixture or returns prohibited fixture content |
| Asynchronous work | Job creation → serialized actor/context → queue → worker → retry | A worker loses the initiating user's authorization context or repeats a protected effect |
| Concurrency | Read/check/write sequence, transaction boundaries, locks, uniqueness, idempotency | Two bounded local operations violate an explicit invariant |
| Extensions and AI features | Plugin/tool selection, retrieval ownership, instruction/data boundary, delegated credentials | Lower-trust content causes a demonstrably unauthorized tool or data action |
Authentication and authorization require separate proofs. A valid JWT says little about whether a user owns the requested object. Check where tenant identity originates, whether membership is current, whether query scoping survives joins and bulk operations, and whether subsequent service calls replace validated identifiers. Inspect alternate endpoints, download URLs, exports, websocket handlers, and scheduled tasks that share the same service.
Taint analysis is a model. Define sources, propagators, sanitizers, and sinks for the actual framework and wrappers. A sanitizer is valid only for the destination context; HTML escaping does not establish filesystem containment. An authorization guard is not equivalent to value sanitization. Validate your rule against a vulnerable fixture and a safe sibling. Semgrep documents these modeling components; its CE analysis does not provide every interprocedural/interfile capability available in paid engines. Semgrep: taint analysis, CLI capabilities.
Reachability is the decisive filter. Record the entry point, attacker role, call sequence, feature flag, build inclusion, configuration, and guard outcomes. Dead code, administrator-only extension loading, a debug-only helper, and a low-privilege production route are different findings. A negative scanner result is not proof that a route is safe. For dynamic dispatch, generated code, reflection, framework magic, or runtime plugin resolution, explain what static analysis did not cover.
Prove the real execution path. A unit-level demonstration that calls an internal helper directly may bypass the very authorization check being assessed. Begin with the supported application interface when claiming application impact. Use instrumentation to explain the path, but reproduce again with security behavior unchanged. Keep the authorized control case beside the failing case.
Dependencies, secrets, and delivery infrastructure#
Give this work its own review pass. Application SAST does not examine who can alter a release, which registry supplies dependencies, or whether a worker image contains additional vulnerable components.
Dependencies and packaged artifacts
Build separate inventories for source/lockfiles, installed runtime dependencies, build/test tooling, and final images or release archives. Record package ecosystem, name, resolved version, origin, direct/transitive relationship, and shipped artifact. An SBOM records composition; a vulnerability matcher associates known advisories. Neither alone proves exploitability. Syft generates SBOMs; OSV-Scanner extracts package information and matches it against vulnerability records. Syft, OSV-Scanner usage.
For each advisory, establish: affected version range, relevant API or feature, application call path, attacker-controlled argument, necessary configuration, deployed inclusion, and security impact. Check distro backports and vendored modifications. A package in a development dependency may affect the build pipeline without shipping in the application; keep those impact stories separate. Avoid submitting “old package version” when the program requires a demonstrated vulnerable use.
For dependency confusion, inspect scoped registry mappings, fallback behavior, namespace ownership, lockfile integrity, URL dependencies, install hooks, and privileged build context. Reproduce resolution behavior using a local registry and synthetic package names. Do not claim real package names, publish to public registries, or trigger target builds as an assumed extension of repository scope.
Secrets
Inspect the working tree and available Git history separately, including release archives and readable build configuration. Classify a finding as fixture, public identifier, non-secret material, or plausible credential. Preserve the file path, commit, secret type, relevant context, and a redacted fingerprint. Ask the owner to validate and rotate plausible exposed credentials; do not log in with them or call provider validation endpoints. Gitleaks' modern interfaces are git, dir, and stdin; detect and protect are deprecated. Redaction is not a substitute for reviewing exported evidence. Gitleaks official documentation.
CI/CD and GitHub Actions
For each workflow, trace event actor → attacker-controlled data/code → executed step → token/environment privilege → release or deployment effect. Enumerate workflow triggers, reusable workflows, local/composite actions, shell interpolation, downloaded artifacts, cache consumers, package-install hooks, credentials, runner isolation, and deployment approvals. Review the action implementation at the pinned revision. GitHub recommends minimum token permissions and immutable commit-SHA action references. GitHub: secure use.
pull_request_target or workflow_run is a review signal, not a complete vulnerability. Determine whether untrusted code or artifacts are actually executed and which privileges are available. Current GitHub documentation describes checkout protections and cache restrictions for pull_request_target; inspect the exact action revision, explicit opt-outs, repository settings, and observed behavior instead of assuming older examples still reproduce. Use a private/local fixture with inert marker output and dummy credentials; request a maintainer-assisted run if real workflow behavior is necessary. GitHub: event semantics, securely using pull_request_target.
Registries, signing, images, and IaC
Map repository permissions to release tags, build inputs, artifacts, registry publishing, and deployment identities. Examine whether untrusted material can enter after review or before signing. Verify provenance against the expected source and builder; authenticated provenance does not prove application correctness. For OIDC, inspect issuer, audience, repository/workflow identity, branch/environment conditions, and the actual role permissions. GitHub's AWS guidance specifically calls for evaluation of the token subject to restrict trusted workflows. GitHub: OIDC in AWS.
Trusted publishing removes reliance on long-lived publish credentials, but its security depends on the configured trusted workflow. Inspect publisher settings and legacy token fallback instead of treating an OIDC badge as sufficient. npm documents workflow identity restrictions; PyPI explicitly analyzes how compromised workflows can affect trusted publishers. npm: trusted publishing, PyPI: security model.
Compare the final image with source expectations: inherited packages, copied secrets, build layers, entrypoint privilege, capabilities, host mounts, and update mechanism. Review IaC defaults, exposed services, wildcard IAM, cross-tenant storage, secret injection, and metadata-service assumptions. A scanner's root-user or wildcard-policy finding becomes a bounty candidate only when it violates an applicable security expectation with defensible impact. Preserve the relevant rendered configuration rather than reporting a template variable whose deployed value is unknown.
Parsers, native code, and fuzzing#
Choose a narrow parser or state transition with realistic attacker input. First reproduce normal usage through its public API. Build an in-process harness that resets state, bounds input size, avoids network/disk side effects, and instruments relevant code. Seed it with a small representative corpus, add format dictionaries when useful, and use ASan/UBSan where appropriate. Track coverage growth, unique root causes, and minimized reproducers rather than raw crash totals. LLVM documents libFuzzer corpus handling, runtime/resource bounds, and crash minimization. LLVM: libFuzzer.
Confirm that a crash belongs to the target rather than your harness. Reproduce on the eligible build, minimize the input, deduplicate by root cause, and identify whether the outcome is memory corruption, bounded parsing failure, or resource exhaustion. State remote/local exposure through a real consumer. Do not convert a local crash into an unsupported remote-code-execution claim; do not fuzz live shared services under this local workflow.
Patch analysis, reporting, and CVEs#
Read security patches to identify the invariant being restored, then inspect equivalent call sites, alternate encodings, older supported branches, and duplicated helpers. Patch-diff work is valuable for learning and identifying incomplete fixes; reproducing an already-disclosed issue may be ineligible or duplicate. Record which version introduced the flaw, which tested versions remain affected, and which patch actually prevents the behavior.
A report package should contain the exact source revision and product version, supported setup, minimal configuration delta, attacker role, reproduction steps, expected/actual results, root-cause path, affected-version evidence, demonstrated impact, and a focused remediation suggestion. Provide synthetic fixture data and a minimal local PoC. For retesting, compare the vulnerable case, authorized behavior, and a nearby variant; report remaining uncertainty instead of extrapolating a version range.
Use the private channel specified by the program or maintainers. Coordinate an advisory and disclosure timing. GitHub allows maintainers to request a CVE within the repository advisory workflow when they do not already have one; requesting an identifier is separate from proving eligibility or earning payment. GitHub: publishing advisories and requesting a CVE.
Local command cheat sheet#
Examples target only an existing local ./repo; they have not been executed against a target. Create ./evidence first and keep it outside the scan root. Preinstall pinned tool versions, inspect inherited configurations, and prepare required rules/databases/packs in a separate connected preparation phase. Run private-code analysis with network egress blocked. Scanner exit codes can represent findings or execution failures; preserve stderr and classify the outcome before interpreting the report.
1. Freeze the source identity. Git reference.
git -C ./repo rev-parse --verify HEAD > ./evidence/commit.txt
2. Inventory source and hidden configuration without reading Git objects. ripgrep guide.
rg --files --hidden -g '!.git' ./repo > ./evidence/files.txt
3. Run reviewed local Semgrep rules. No registry rules are fetched by this command. Semgrep CLI.
semgrep scan --oss-only --config ./rules --metrics=off \
--disable-version-check --sarif \
--output ./evidence/semgrep.sarif ./repo
The CE engine uses LGPL-2.1; Semgrep-maintained registry rules have a separate Semgrep Rules License, and third-party rules have their own licenses. Internal research and embedding rules into a commercial scanning service are different uses. Prefer original local rules or rules whose terms fit the intended use. Semgrep licensing, Rules License.
4. Review local Git history for secrets. A shallow clone limits historical coverage. Inspect the output before sharing. Gitleaks.
gitleaks git ./repo --redact=100 --report-format json \
--report-path ./evidence/gitleaks.json
5. Inventory local packages. Syft output formats.
syft dir:./repo -o cyclonedx-json=./evidence/sbom.cdx.json
6. Match dependencies using previously cached OSV ecosystem databases. Full --offline prevents network use; --offline-vulnerabilities alone still permits other network features. Offline mode lacks commit-level matching. OSV source scanning, offline mode.
osv-scanner scan source --offline -r --format json \
./repo > ./evidence/osv.json
7. Inspect dependencies and IaC with prepared Trivy databases/checks. These flags independently disable online resolution, updates, notices, and telemetry. Missing caches or unsupported flags on older releases require preparation/version correction, not silent fallback to online scanning. Trivy filesystem CLI.
trivy fs --scanners vuln,misconfig --offline-scan \
--skip-db-update --skip-java-db-update --skip-check-update \
--skip-version-check --disable-telemetry \
--format json --output ./evidence/trivy.json ./repo
8. Create and query a Python CodeQL database. Requires a prepared local query suite and its dependencies; language/build setup differs for other projects. Database preparation, analysis.
codeql database create ./evidence/codeql-python \
--language=python --source-root=./repo
codeql database analyze ./evidence/codeql-python \
./queries/python-security.qls --format=sarif-latest \
--output=./evidence/codeql.sarif
CodeQL is free for public repositories under its terms. Private organization repositories require the applicable GitHub plan and GitHub Code Security license; possession of source does not grant unrestricted CodeQL use. The query libraries' open-source license does not replace the CLI terms. GitHub: CodeQL availability and license.
Exit checklist#
- Repository, product edition, version, scope, and reporting channel recorded.
- Working baseline, synthetic identities, reset procedure, and config delta preserved.
- Priority entry points mapped to authentication, authorization, and sensitive operations.
- Every promoted finding has attacker control, reachability, prerequisites, and impact evidence.
- Source, runtime, build, and shipped dependency inventories distinguished.
- Secrets redacted; no discovered credential used for verification.
- CI events, artifact/cache consumers, publishing identity, images, and IaC reviewed.
- Scanner errors, excluded files, stale databases, and unresolved models recorded.
- Local PoC minimizes side effects and includes a legitimate control case.
- Prior advisories checked; tested versions distinguished from inferred versions.
- Report and patch retest can be reproduced from preserved inputs.
GitLab and provider-specific CI branches#
The shared release audit applies to GitLab, but GitHub event names and defaults do not transfer directly. Use GitLab pipeline boundaries to trace the configuration revision, execution project, trigger actor, protected resources, job tokens, downstream inputs and runner state. Keep these three identities separate in the report; a merge request does not by itself prove execution with parent-project authority.
For jobs that deploy into Azure or GCP, continue through provider-specific identity and storage boundaries. Record the exact token-to-cloud-principal binding and final synthetic resource operation. A cloud login inside a CI job is one edge in the chain, not a complete release or tenant-isolation proof.
Source registry#
All sources were accessed on 2026-09-11. Unless a date is listed, the cited documentation is undated/living documentation; tool and service behavior must be checked again when used.
| Publisher | Source | Publication/update date | Supports |
|---|---|---|---|
| OWASP | Secure Code Review | Undated | Manual review, logic/data-flow emphasis |
| GitHub | Privately reporting a vulnerability | Undated | Private reporting availability |
| GitHub | Repository security advisories | Undated | Private collaboration/disclosure |
| GitHub | Publishing an advisory | Undated | CVE request workflow |
| Semgrep | Taint analysis | Undated | Sources, sinks, propagators, sanitizers |
| Semgrep | CLI reference | Undated | Local rules, flags, engine capabilities |
| Semgrep | Licensing and Rules License | Undated | CE and rule licensing distinctions |
| Gitleaks maintainers | Official repository documentation | Undated | Current commands, history, redaction |
| Anchore | Syft and output formats | Undated | SBOM inventory and output syntax |
| OSV usage, source scanning, offline mode | Undated | Version matching, recursion, offline limits | |
| Aqua Security | Trivy filesystem reference | Undated | Scanners and individual offline/update flags |
| GitHub | Secure use reference | Undated | Least privilege, immutable action references |
| GitHub | Workflow events and pull_request_target guidance | Undated | Event trust boundaries and current protections |
| GitHub | OIDC in AWS | Undated | Subject/identity restrictions |
| npm | Trusted publishing | Updated 2026-09-03 | Publisher identity and authentication |
| Python Package Index | Trusted publisher security model | Undated | Workflow compromise and publishing trust |
| LLVM Project | libFuzzer | Undated | Harness, corpus, bounds, minimization |
| GitHub | CodeQL CLI, database preparation, analysis | Undated | Eligibility and local CLI workflow |
| Git Project | git-rev-parse | Undated | Commit identity command |
| Andrew Gallant/ripgrep contributors | ripgrep guide | Undated | Local file inventory flags |