Skip to content

fix(security): redact all proxy credentials and drop the phantom ecdsa dependency - #1438

Merged
groupthinking merged 4 commits into
mainfrom
claude/event-relay-blockers-1k020k
Aug 7, 2026
Merged

fix(security): redact all proxy credentials and drop the phantom ecdsa dependency#1438
groupthinking merged 4 commits into
mainfrom
claude/event-relay-blockers-1k020k

Conversation

@groupthinking

Copy link
Copy Markdown
Owner

Canonical issue

Closes #1437

Outcome

Lands the two security fixes from #1378 that were orphaned by the secret-purge force-push. Both original PRs (#1118, #1156) share no ancestry with main, so neither could be rebased or cherry-picked — both were reimplemented against current main, and in one case the original premise had to be re-verified before it could be trusted.

Proxy credentials stop leaking into logs. redact_proxy_credentials bailed out unless the configured WEBSHARE_PROXY_URL appeared byte-for-byte, so a host that urllib3 had case-normalised, a percent-encoded form echoed by yt-dlp, a CalledProcessError argv repr, or any other proxy variable passed straight through into log output.

The ecdsa advisory is cleared by removing the path to it. GHSA-wj6h-64fc-37mp has no patched release, so upgrading was never an option. python-jose was its sole dependent and is never imported.

Scope

Risk

  • Risk level: low
  • Failure mode, redaction: over-redaction destroying diagnostics. Explicitly tested against — an @ in a path, query, or fragment, and a bare host:port, all pass through untouched, and a match can never span two URLs. The helper is also now total: a hostile __str__ yields <unprintable error>, a redaction failure yields <redaction failed>. Both fail closed rather than raising inside an except block or returning text that cannot be vouched for.
  • Failure mode, dependency: removing something actually needed. uv lock also removed rsa; verified its only dependent was python-jose too, and that google-auth in this resolution needs cryptography + pyasn1-modules, not rsa. Zero residual references to ecdsa, rsa, or python-jose in uv.lock, and zero AST-level imports of any of them.
  • Rollback: git revert. Two independent commits; either can be reverted alone.

Verification

Head de6f36cae.

  • The new tests are non-vacuous22 of 38 fail against the implementation on main, all 38 pass after the change. Checked by restoring main's two source files and re-running.
  • Every leak reproduced before fixing — five cases, listed in Two unlanded security fixes orphaned by the force-push: proxy credential leak and the ecdsa advisory #1437, each confirmed leaking on main and confirmed clean after.
  • Both helper copies tested — the suite parametrises over the canonical and shared/libs implementations, so neither can drift into leaking alone. Regex source verified byte-identical across the two.
  • python-jose really is phantom — verified by AST parse, not grep: zero jose imports in src/, shared/, scripts/. The one textual hit is inside a string template.
  • Generated projects unaffectedcode_generator.py writes its own requirements.txt pinning python-jose[cryptography]==3.3.0 for the project it emits.
  • Runtime intact — backend imports and serves 11 routes; code_generator (which holds the template) imports.
  • Full unit suite — 8,035 passed, 5 xpassed, 0 failed.
  • Lint — no new ruff findings; shared/libs/youtube_proxy.py holds at 8 pre-existing, unchanged.

Production evidence

Not applicable — no runtime behaviour changes for users. The redaction change only affects what is written to logs (strictly less credential material), and the dependency change removes packages that were never imported. Under MERGE_POLICY.md gate 4, previews gate apps/web/**; this touches none.

A correction worth recording

The original #1156 was titled "drop phantom python-jose". On current main that premise no longer looked true — grep finds from jose import JWTError, jwt in code_generator.py. Had I trusted the old PR's framing, I would have either skipped the fix as stale or removed a dependency that appeared to be in use.

Parsing the file settled it: the hit is inside a string literal the generator emits, and there are zero jose imports at AST level. The premise held; the grep was just the wrong instrument. Same shape as the merge-base lesson from the branch audit — the cheap signal disagreed with the true one.

Agent handoff


Generated by Claude Code

claude added 2 commits August 7, 2026 18:54
redact_proxy_credentials bailed out unless the configured WEBSHARE_PROXY_URL
appeared in the text byte-for-byte:

    if not url or url not in text:
        return text

It is called from exception handlers that log subprocess and HTTP failures, so
anything it skipped was written to logs verbatim. Reproduced leaks, all with
WEBSHARE_PROXY_URL=http://user:s3cr3t@proxy.internal:8080:

  * host case-normalised by requests/urllib3 when re-rendering the URL
        connect to http://user:s3cr3t@PROXY.INTERNAL:8080   -> leaked
  * percent-encoded variant echoed back by yt-dlp
        http://user:s3cr3t%40x@proxy.internal:8080          -> leaked
  * a different proxy variable entirely, never equal to the configured one
        HTTPS_PROXY=http://bob:hunter2@corp.proxy:3128      -> leaked
  * a CalledProcessError repr of the argv
        ['yt-dlp','--proxy','http://u:p4ss@h:1']            -> leaked

Adds a second, generic pass: a scheme://user[:password]@ sweep that redacts
credentials regardless of which variable they came from or how they were
rendered. The exact-match pass is kept and runs first, because it preserves the
host so operators can still tell which proxy was in play.

The userinfo classes exclude the authority delimiters (whitespace, "/", "?",
"#") so a path or query containing "@" is never mistaken for credentials and a
match cannot span two URLs, but they permit a literal "@". RFC 3986 requires
"@" in userinfo to be percent-encoded while real proxy values carry a raw one;
because the classes are greedy the engine settles on the LAST "@" in the
authority, so http://user:pa@ss@host is consumed whole instead of the match
stopping at the first separator and leaving "ss@host" behind (#1113).

The helper now also accepts non-str input and never raises -- it is called from
except blocks, where raising would mask the original error. A hostile __str__
yields <unprintable error>; a redaction failure yields <redaction failed>.
Both fail closed rather than returning text that cannot be vouched for.

Applied to the canonical helper and to the drifted standalone copy in
shared/libs/ that the importlib fallback path uses, whose three logger calls in
the retry handler are the actual leak site.

Adds tests/unit/test_proxy_utils.py -- the module had no test coverage at all.
It parametrises over both implementations so neither can drift into leaking
alone, and covers over-redaction (an "@" in a path, query, or fragment, and a
bare host:port) since destroying diagnostics is its own failure.

Verified non-vacuous: 22 of the 38 new tests fail against the implementation on
main and all 38 pass after this change. Full unit suite 8035 passed, 0 failed.
No new ruff findings.

Re-cut from PR #1118, which was orphaned by the secret-purge force-push and
shares no ancestry with main. Tracked in #1378.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YcHjCZ6pGn6A5BeeoZ6eZi
…sory

python-jose was declared in pyproject.toml and requirements.txt but is never
imported by this codebase. The only occurrence is inside a string template in
backend/code_generator.py:

    auth_imports = '''
    from jose import JWTError, jwt
    from passlib.context import CryptContext'''

That text is written into projects the generator emits, and the generator
writes those projects their own requirements.txt pinning python-jose (line
468). Parsing the file confirms it: zero jose imports at AST level anywhere in
src/, shared/, or scripts/.

Declaring it pulled in ecdsa, whose GHSA-wj6h-64fc-37mp has no patched release
-- so the advisory could not be resolved by upgrading, only by removing the
path to it. python-jose is ecdsa's sole dependent in the resolution, so
dropping it removes the advisory outright.

uv lock removes three packages: ecdsa, python-jose, and rsa. rsa goes because
python-jose was its only dependent too -- google-auth in this resolution
depends on cryptography and pyasn1-modules, not rsa. Verified zero residual
references to all three in uv.lock and zero AST-level imports of any of them.

Generated projects are unaffected: they install from the requirements.txt the
generator writes for them, which still pins python-jose[cryptography]==3.3.0.

Left in place: passlib is the same template-only case and could be dropped on
the same reasoning, but it carries no advisory and removing it is not needed
here. Noted in both manifests rather than changed unilaterally.

Verified: backend imports and serves 11 routes; code_generator (which holds the
template) imports; full unit suite 8035 passed, 0 failed.

Re-cut from PR #1156, which was orphaned by the secret-purge force-push and
shares no ancestry with main. Tracked in #1378.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YcHjCZ6pGn6A5BeeoZ6eZi
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v0-uvai Ready Ready Preview, v0 Aug 7, 2026 8:45pm

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • [‘architecture-gap’, ‘bug’, ‘ci-cd’, ‘ci/cd’, ‘copilot-rabbit’, ‘documentation’, ‘duplicate’, ‘enhancement’, ‘frontend’, ‘github_actions’, ‘good first issue’, ‘help wanted’, ‘high-priority’, ‘invalid’, ‘javascript’, ‘ml-model’, ‘needs-triage’, ‘pipeline-critical’, ‘placeholder-code’, ‘priority:high’, ‘python’, ‘python:uv’, ‘question’, ‘styling’, ‘tests’, ‘v0’]

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: dcf766d7-1f33-4092-ab6d-771abb2f2990

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@groupthinking groupthinking added bug Something isn't working high-priority Urgent - blocks revenue or core functionality python labels Aug 7, 2026 — with Claude
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

🔍 PR Validation

⚠️ Large PR detected (589 lines changed)

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA 98187c4.
Ensure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice.

OpenSSF Scorecard

PackageVersionScoreDetails
pip/pyjwt 2.13.0 UnknownUnknown

Scanned Files

  • requirements.txt
  • uv.lock

CI caught what my local run missed: three tests in test_code_generator.py
failed with ModuleNotFoundError: No module named 'jose'.

TestGeneratedFastAPIBehaviour does not inspect the generated source, it
*executes* the FastAPI app that code_generator emits -- deliberate regression
cover for #1257, where the template shipped placeholder endpoints returning 200
so a generated project passed a naive smoke test while being non-functional.
The emitted app imports jose, so removing the dependency broke those tests.

I checked src/, shared/ and scripts/ for jose imports and found none, but never
checked tests/. My local suite passed only because the venv still had
python-jose installed from an earlier editable install; CI installs fresh.

Moving python-jose to the dev extra would have fixed CI while leaving ecdsa in
uv.lock, so the advisory would have survived -- and every generated project
would still inherit it. Instead the template now emits PyJWT:

    -from jose import JWTError, jwt
    +import jwt
    +from jwt import PyJWTError

encode/decode signatures are identical; only the exception type changes. PyJWT
is maintained and depends on nothing with an open advisory, whereas
python-jose's ecdsa (GHSA-wj6h-64fc-37mp) has no patched release.

Generated projects get pyjwt>=2.10.1 in their requirements.txt instead of
python-jose[cryptography]==3.3.0. This changes generator output -- called out
explicitly on the PR so it can be objected to -- but a generated project should
not ship a known-vulnerable transitive dependency.

pyjwt is added to the dev extra, not the runtime dependencies: EventRelay never
imports it, only the generated app the tests execute does.

Verified in a venv with jose uninstalled, matching CI: 96 code_generator tests
pass, full unit suite 8035 passed, 0 failed. ecdsa, python-jose and rsa all
absent from uv.lock; pyjwt present.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YcHjCZ6pGn6A5BeeoZ6eZi

Copy link
Copy Markdown
Owner Author

CI caught a real miss, and the fix changes generator output — please read

test and Generate and Upload Coverage failed on de6f36cae with:

FAILED tests/unit/test_code_generator.py::TestGeneratedFastAPIBehaviour::test_unimplemented_endpoints_fail_loudly - ModuleNotFoundError: No module named 'jose'
FAILED tests/unit/test_code_generator.py::TestGeneratedFastAPIBehaviour::test_token_helpers_are_real_implementations   - ModuleNotFoundError: No module named 'jose'
FAILED tests/unit/test_code_generator.py::TestGeneratedFastAPIBehaviour::test_signing_fails_closed_without_a_secret_key - ModuleNotFoundError: No module named 'jose'

What I got wrong

I verified "zero jose imports at AST level" across src/, shared/ and scripts/ — but never checked tests/. TestGeneratedFastAPIBehaviour does not inspect the generated source, it executes the emitted FastAPI app. That app imports jose, so removing the dependency broke it.

My local suite passed 8,035/0 only because the venv still had python-jose installed from an earlier editable install. CI installs fresh, so it saw the truth and I didn't. The claim in the PR body that the dependency was unused was too broad — it was unused by EventRelay's runtime, not by its tests.

Those tests are regression cover for #1257, where the template shipped placeholder endpoints returning 200 so a generated project passed a naive smoke test while being non-functional. Making them skip when jose is unavailable would have restored green by making them vacuous — precisely the failure mode this repo has been digging out of. Not an option.

Why not just move it to the dev extra

That fixes CI while leaving ecdsa in uv.lock, so the advisory survives — and every generated project still inherits it. It would have made the PR green without achieving what the PR is for.

What I did instead

The template now emits PyJWT:

-from jose import JWTError, jwt
+import jwt
+from jwt import PyJWTError

encode/decode signatures are identical; only the exception type changes. PyJWT is maintained and has no open advisory; python-jose pulls ecdsa, whose GHSA-wj6h-64fc-37mp has no patched release.

This changes generator output. Projects generated with the authentication feature now get pyjwt>=2.10.1 in their requirements.txt instead of python-jose[cryptography]==3.3.0. I judged that in scope — the whole point is clearing an advisory, and a generated project should not ship a known-vulnerable transitive dependency — but it is a product-behaviour change, so say the word and I'll revert to the dev-extra approach instead, accepting that ecdsa stays in the lockfile.

pyjwt goes in the dev extra, not the runtime dependencies: EventRelay never imports it; only the generated app the tests execute does.

Verification, this time in an environment matching CI

I uninstalled python-jose, ecdsa and rsa from the venv first, so import jose genuinely fails — the condition that masked the bug is gone.

  • tests/unit/test_code_generator.py96 passed
  • Full unit suite — 8,035 passed, 0 failed
  • uv.lockecdsa, python-jose, rsa all absent; pyjwt present
  • ruff — clean on every touched file

Head is now 5cefd7d8e.


Generated by Claude Code

@groupthinking
groupthinking marked this pull request as ready for review August 7, 2026 20:43
@groupthinking
groupthinking merged commit a66e1c5 into main Aug 7, 2026
23 of 27 checks passed
@groupthinking
groupthinking deleted the claude/event-relay-blockers-1k020k branch August 7, 2026 20:43
@linear-code

linear-code Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

GRV-363

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working high-priority Urgent - blocks revenue or core functionality python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Two unlanded security fixes orphaned by the force-push: proxy credential leak and the ecdsa advisory

2 participants