fix(security): stop proxy credentials leaking from subprocess errors - #1118
fix(security): stop proxy credentials leaking from subprocess errors#1118groupthinking wants to merge 5 commits into
Conversation
…1113) `WEBSHARE_PROXY_URL` carries `user:password` in its userinfo. Two paths leaked it verbatim: 1. `enhanced_video_processor._get_openai_whisper_transcript` runs yt-dlp via `subprocess.run(..., check=True)`. The resulting `CalledProcessError` stringifies the whole argv, including `--proxy http://user:pass@host`. That string was written to `logger.warning` (CWE-532) *and* returned to the caller in the `error` field of the response (CWE-209). 2. `robust._get_metadata_ytdlp` raised `yt-dlp failed: {result.stderr}`; yt-dlp echoes the `--proxy` value back on stderr for connection failures. `TimeoutExpired` from the same call site stringifies the argv too. Separately, `get_proxy_url()` documented "malformed => None" but did not honour it: `urllib.parse` raises `ValueError` on an unterminated IPv6 literal at parse time, and on a non-numeric or out-of-range port when `.port` is read. The exception escaped to callers that log it, which put the offending URL — credentials and all — into the log a third way. Changes: - `utils/proxy.get_proxy_url` contains `ValueError` from both `urlparse` and the `.port` access, adds `socks5h` to the allowed schemes, and keeps the URL out of the "malformed" warning. - `utils/proxy.redact_proxy_credentials` now accepts any object, never raises (it runs inside `except` blocks, where a failure would mask the original error), and sweeps in two passes: an exact replacement of the configured env value that preserves host:port for triage, then a generic `scheme://user:pass@` regex for normalised stderr echoes, argv dumps and other proxy variables. The user/password classes exclude `/`, so a path containing `@` is not over-redacted. - Both leak sites redact before logging or returning. - `shared/libs/youtube_proxy.py` (a drifted duplicate, loaded both as a package and standalone via importlib) now delegates to the canonical helper with an equivalent local fallback, matching the pattern already used in `gemini_video_master_agent.py`. This also fixes a latent `UnboundLocalError` on a portless proxy URL. 16 of the 29 new tests in `tests/unit/test_proxy_utils.py` fail against the pre-fix code. Full `tests/unit` run shows no new failures. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are limited based on label configuration. 🏷️ Required labels (at least one) (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughSummary by CodeRabbit
WalkthroughChangesProxy security hardening
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant ProxyConsumer
participant ProxyUtilities
participant WhisperOrYtDlp
participant LoggerOrCaller
ProxyConsumer->>ProxyUtilities: validate proxy URL
ProxyUtilities-->>ProxyConsumer: proxy configuration or direct fallback
WhisperOrYtDlp-->>ProxyConsumer: return failure details
ProxyConsumer->>ProxyUtilities: redact credentials
ProxyUtilities-->>LoggerOrCaller: sanitized error
Possibly related issues
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 5 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
Warning Review ran into problems🔥 ProblemsThese MCP integrations need to be re-authenticated in the Integrations settings: Sentry Comment |
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Snapshot WarningsEnsure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice. Scanned FilesNone |
🔍 PR Validation |
Agent Completion Truth Gate: NOT_APPLICABLEEvidence agrees. Machine-readable verdict{
"details": {},
"reasons": [],
"verdict": "not_applicable"
} |
9c8fadb to
9e8f5fd
Compare
There was a problem hiding this comment.
Pull request overview
Fixes proxy credential leakage and makes billing quota tests deterministic.
Changes:
- Redacts proxy credentials and safely validates malformed proxy URLs.
- Adds proxy security regression coverage.
- Strengthens billing gating tests with mocked AI responses and exact assertions.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/youtube_extension/utils/proxy.py |
Adds validation and credential redaction. |
shared/libs/youtube_proxy.py |
Aligns standalone proxy handling. |
src/youtube_extension/backend/services/youtube/adapters/robust.py |
Redacts yt-dlp failures. |
src/youtube_extension/backend/enhanced_video_processor.py |
Sanitizes Whisper errors. |
tests/unit/test_proxy_utils.py |
Tests proxy validation and redaction. |
tests/unit/test_robust_youtube_service.py |
Tests stderr credential hygiene. |
tests/unit/test_enhanced_video_processor.py |
Tests subprocess error sanitization. |
apps/web/src/app/api/__tests__/billing-chat-gating.test.ts |
Expands deterministic quota tests. |
|
@coderabbitai full review The auto-review was skipped at open time because the required label was not yet present; the PR now carries Generated by Claude Code |
|
✅ Action performedFull review finished. |
groupthinking
left a comment
There was a problem hiding this comment.
Independent review (code-reading pass) — no blocking issues
Reviewed the security-critical redaction paths closely, since that's where a subtle bug would be worst.
Verified correct
- Two-pass redaction doesn't over-redact. After the exact-URL pass rewrites
scheme://user:pass@host:port→scheme://host:port, the generic_USERINFO_REre-scans the result. It does not re-match, because the pattern requires a trailing@after the userinfo and there is none once credentials are stripped — so the host:port survives for triage. Traced by hand; matches thetest_exact_configured_url_is_redacted_but_host_preservedexpectation. - Path-embedded
@is preserved (https://example.com/users/a@b.txt):user=[^\s/:@]+then the required@fails at the/, and there's no second://anchor, so no match. Good — real URLs with@in a path aren't mangled. - The #1113
.portescape is properly closed. Readingparsed.portinside thetrycatches bothValueErrorsites — the parse-time one (unterminated IPv6[::1) and the.port-access one (non-numeric / out-of-range port). The documented "malformed ⇒ None" contract now actually holds, and the warning names the variable, never the value. - Frontend de-vacuuming is a genuine correctness fix.
expect(res.status).not.toBe(402)was satisfied by the 503 "gateway not configured" path, so on a machine without a key the loop asserted nothing.toBe(200)+generateTextcall-count assertions (and the explicit no-gateway-key 503 case) close that hole, and mockingai'sgenerateTextremoves the live-network flakiness.
Minor, non-blocking
- An unencoded
@inside a password (http://u:p@ss@host) would leave thessremainder unmasked, sincepassword=[^\s/@]*stops at the first@. This is spec-invalid (should be%40) and Webshare credentials are alphanumeric, so there's no practical exposure — flagging for completeness, not requesting a change.
CI
Required checks are green (build, lint-python, lint-frontend, bandit, CodeQL, Security Scan - python, python-safety, gitleaks, npm-audit, dependency-review). The red check-runs (PR Governance, Agent completion enforcement, Canonical issue and evidence) are the repo's own governance gates, not defects in this diff.
Leaving the approve/merge decision to a maintainer — main is protected and this wasn't opted into auto-merge.
Generated by Claude Code
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/youtube_extension/backend/enhanced_video_processor.py`:
- Around line 323-328: Make both redact_proxy_credentials() implementations
non-raising by guarding text conversion with proper exception handling and
returning a safe placeholder when str(text) fails. Apply this root-cause fix for
the Whisper fallback at
src/youtube_extension/backend/enhanced_video_processor.py:323-328, the yt-dlp
fallback warning at
src/youtube_extension/backend/services/youtube/adapters/robust.py:147-149, and
sanitized stderr handling at
src/youtube_extension/backend/services/youtube/adapters/robust.py:176-180; these
handlers must continue logging or returning sanitized details without masking
the original failure.
In `@src/youtube_extension/utils/proxy.py`:
- Around line 113-114: Guard the string conversion in both redaction helpers: in
src/youtube_extension/utils/proxy.py lines 113-114 and
shared/libs/youtube_proxy.py lines 121-122, catch failures from converting text
to str and return the same safe fixed placeholder instead of propagating the
conversion exception. Keep normal string conversion unchanged when it succeeds.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: d85644e8-4336-4510-8553-eac4e7ced31e
⛔ Files ignored due to path filters (3)
tests/unit/test_enhanced_video_processor.pyis excluded by!tests/**tests/unit/test_proxy_utils.pyis excluded by!tests/**tests/unit/test_robust_youtube_service.pyis excluded by!tests/**
📒 Files selected for processing (4)
shared/libs/youtube_proxy.pysrc/youtube_extension/backend/enhanced_video_processor.pysrc/youtube_extension/backend/services/youtube/adapters/robust.pysrc/youtube_extension/utils/proxy.py
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
groupthinking/uvai-skills(manual)
📜 Review details
⏰ Context from checks skipped due to timeout. (3)
- GitHub Check: Generate and Upload Coverage
- GitHub Check: trivy
- GitHub Check: test
⚠️ CI failures not shown inline (2)
GitHub Actions: Agent completion enforcement / Agent completion enforcement: fix(security): stop proxy credentials leaking from subprocess errors
Conclusion: failure
##[group]Run actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
script: const fs = require('fs');
const pull = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: Number(process.env.PR)
});
let verdict = {
conclusion: 'failure',
reason: 'verifier_did_not_publish',
details: {}
};
try {
verdict = JSON.parse(fs.readFileSync(
'enforcement-verdict.json', 'utf8'
));
} catch (error) {
core.warning(error.message);
}
const conclusion = verdict.conclusion === 'success'
? 'success'
: 'failure';
const summary = JSON.stringify(verdict);
await github.rest.checks.create({
owner: context.repo.owner,
repo: context.repo.repo,
name: 'Agent completion enforcement',
head_sha: pull.data.head.sha,
status: 'completed',
conclusion,
output: {
title: conclusion === 'success'
? 'Trusted evidence verified'
: 'Trusted evidence blocked',
summary: summary.slice(0, 60000)
}
});
if (conclusion !== 'success') {
core.setFailed(verdict.reason || 'trusted evidence blocked');
}
github-***REDACTED_SECRET_ASSIGNMENT***
debug: false
user-agent: actions/github-script
result-encoding: json
retries: 0
retry-exempt-status-codes: 400,401,403,404,422
env:
PR: 1118
##[endgroup]
##[error]missing_trusted_publication
GitHub Actions: Agent completion enforcement / 0_Agent completion enforcement.txt: fix(security): stop proxy credentials leaking from subprocess errors
Conclusion: failure
##[group]Run actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
script: const fs = require('fs');
const pull = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: Number(process.env.PR)
});
let verdict = {
conclusion: 'failure',
reason: 'verifier_did_not_publish',
details: {}
};
try {
verdict = JSON.parse(fs.readFileSync(
'enforcement-verdict.json', 'utf8'
));
} catch (error) {
core.warning(error.message);
}
const conclusion = verdict.conclusion === 'success'
? 'success'
: 'failure';
const summary = JSON.stringify(verdict);
await github.rest.checks.create({
owner: context.repo.owner,
repo: context.repo.repo,
name: 'Agent completion enforcement',
head_sha: pull.data.head.sha,
status: 'completed',
conclusion,
output: {
title: conclusion === 'success'
? 'Trusted evidence verified'
: 'Trusted evidence blocked',
summary: summary.slice(0, 60000)
}
});
if (conclusion !== 'success') {
core.setFailed(verdict.reason || 'trusted evidence blocked');
}
github-***REDACTED_SECRET_ASSIGNMENT***
debug: false
user-agent: actions/github-script
result-encoding: json
retries: 0
retry-exempt-status-codes: 400,401,403,404,422
env:
PR: 1118
##[endgroup]
##[error]missing_trusted_publication
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{py,js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{py,js,jsx,ts,tsx}: Use Python 3.9+ and Node 18+ for development
Never hardcode API keys, database URLs, or secrets in code
Make minimal, surgical changes and avoid deleting working code unless fixing security issues
Files:
src/youtube_extension/backend/services/youtube/adapters/robust.pysrc/youtube_extension/backend/enhanced_video_processor.pysrc/youtube_extension/utils/proxy.pyshared/libs/youtube_proxy.py
**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Always use type hints for Python functions
Use Black formatter with 88 character line length for Python code
Follow PEP 8 conventions for Python code
Use descriptive variable names and add docstrings to all public functions in Python
Group imports in Python: standard library, third-party, local
Use SQLAlchemy ORM and never write raw SQL queries
Use environment variables via os.getenv() or pydantic-settings to access configuration
Wrap database operations in try-except blocks and use context managers or FastAPI dependencies for connection cleanup
Implement comprehensive error handling with proper logging in all functions
Version APIs using /api/v1/ prefix for stability
Use JSON-RPC 2.0 protocol for all MCP communication
Follow the single-flow workflow: YouTube link → context extraction → agent dispatch → outputs
Use context managers for resource management in Python code
Implement comprehensive input validation and sanitize outputs for security
Use parameterized queries and SQLAlchemy ORM to prevent SQL injection
Define API request/response models using Pydantic for FastAPI endpoints
Store the single unified workflow as the only workflow; never introduce alternate flows or manual triggers
Use SQLAlchemy with connection pooling for database connections and manage sessions with context managers
Provide sensible defaults for non-sensitive configuration in Python settings
Maintain backward compatibility and do not break existing API endpoints
Include comprehensive logging for debugging in MCP implementations
Files:
src/youtube_extension/backend/services/youtube/adapters/robust.pysrc/youtube_extension/backend/enhanced_video_processor.pysrc/youtube_extension/utils/proxy.pyshared/libs/youtube_proxy.py
⚙️ CodeRabbit configuration file
Python backend code. Check for type hints, proper exception handling, async context manager usage, and potential blocking calls in async functions. Flag any bare except clauses or missing timeout parameters on network calls. CRITICAL: Flag any file that contains placeholder/stub implementations — especially in unified_ai_sdk. Any class or function that says "TODO: Replace with production implementation" or returns mock/fake data must be flagged as a blocking issue. Flag any code generation output that reaches users without AST validation or syntax checking.
Files:
src/youtube_extension/backend/services/youtube/adapters/robust.pysrc/youtube_extension/backend/enhanced_video_processor.pysrc/youtube_extension/utils/proxy.pyshared/libs/youtube_proxy.py
**/*.{py,js,ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Maintain >80% code coverage for new features
Files:
src/youtube_extension/backend/services/youtube/adapters/robust.pysrc/youtube_extension/backend/enhanced_video_processor.pysrc/youtube_extension/utils/proxy.pyshared/libs/youtube_proxy.py
**/*.{py,ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{py,ts,tsx}: Keep frontend and backend data models synchronized using matching Pydantic (backend) and TypeScript (frontend) interfaces
Use type-safe interfaces for backend-frontend data exchange
**/*.{py,ts,tsx}: Production code must use real behavior only: no mock delays, fake data, or simulated responses.
Maintain strict type safety: mypy strict mode for Python and TypeScript strict mode for the frontend.
Name events using the<domain>.<entity>.<action>format.
Files:
src/youtube_extension/backend/services/youtube/adapters/robust.pysrc/youtube_extension/backend/enhanced_video_processor.pysrc/youtube_extension/utils/proxy.pyshared/libs/youtube_proxy.py
**/*
📄 CodeRabbit inference engine (Custom checks)
**/*: Strictly verify that GitHub Copilot has explicitly reviewed and approved the pull request; human approvals alone must not satisfy this check.
Before allowing a merge, require thecopilot-rabbitlabel and AI-generated unit tests committed alongside the code changes; fail the check if either is missing.For Vercel-specific work, include
https://vercel.com/docs/llms-full.txtin the AI assistant context set.
Files:
src/youtube_extension/backend/services/youtube/adapters/robust.pysrc/youtube_extension/backend/enhanced_video_processor.pysrc/youtube_extension/utils/proxy.pyshared/libs/youtube_proxy.py
**/*.{py,pyw}
📄 CodeRabbit inference engine (AGENTS.md)
Write Python code to remain compatible with Linux and Windows where possible, including correct handling of
asyncioevent loops.
Files:
src/youtube_extension/backend/services/youtube/adapters/robust.pysrc/youtube_extension/backend/enhanced_video_processor.pysrc/youtube_extension/utils/proxy.pyshared/libs/youtube_proxy.py
src/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
src/**/*.py: Format Python code with Black using an 88-character line length.
Sort Python imports with isort using the Black profile.
Run Ruff with E, W, F, I, B, C4, and UP rules; E501 is ignored.
Use mypy strict mode; untyped function definitions are disallowed.
Target Python 3.9 or newer.
Validate backend inputs with Pydantic and sanitize subprocess arguments.
Use Anthropic SDK featuresthinking={"type": "adaptive"}andoutput_config={"effort": "..."}withanthropic>=0.105.0; do not addTypeErrorfallbacks for these parameters.Use the service container dependency-injection pattern in
backend/containers/.
Files:
src/youtube_extension/backend/services/youtube/adapters/robust.pysrc/youtube_extension/backend/enhanced_video_processor.pysrc/youtube_extension/utils/proxy.py
**/*.{py,ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Do not commit secrets; store keys and credentials in gitignored
.envfiles.
Files:
src/youtube_extension/backend/services/youtube/adapters/robust.pysrc/youtube_extension/backend/enhanced_video_processor.pysrc/youtube_extension/utils/proxy.pyshared/libs/youtube_proxy.py
**/*.{py,pyi}
📄 CodeRabbit inference engine (GEMINI.md)
**/*.{py,pyi}: Use Black formatting with an 88-character line length for Python code.
Use Ruff with rules E, W, F, I, B, C4, and UP for Python linting.
Use strict mypy type checking; do not define untyped functions.
Target Python 3.9 or newer.
Validate Python inputs with Pydantic.
Sanitize subprocess arguments before execution.
Do not add mock delays or fake data to production Python code; production runs in REAL_MODE_ONLY.
Keep secrets out of Python source code; load keys and credentials from environment variables instead.
Use absolute imports that resolve withPYTHONPATH=srcin the Python backend.
Files:
src/youtube_extension/backend/services/youtube/adapters/robust.pysrc/youtube_extension/backend/enhanced_video_processor.pysrc/youtube_extension/utils/proxy.pyshared/libs/youtube_proxy.py
**/*.{py,pyi,ts,tsx}
📄 CodeRabbit inference engine (GEMINI.md)
**/*.{py,pyi,ts,tsx}: Preserve the single workflow: YouTube link → transcript → events → agents → outputs; do not introduce alternative flows or manual triggers that bypass it.
Use event names following<domain>.<entity>.<action>, such asyoutube.video.captured.
Make surgical, precise changes and do not delete working code without justification.
Files:
src/youtube_extension/backend/services/youtube/adapters/robust.pysrc/youtube_extension/backend/enhanced_video_processor.pysrc/youtube_extension/utils/proxy.pyshared/libs/youtube_proxy.py
🔍 Remote MCP GitHub Copilot, Linear
Additional review context
-
Repository scope: PR
#1118is ingroupthinking/EventRelay, with one commit (9e8f5fd) and seven changed files. Linear taskGRV-189links both this issue and the olderYOUTUBE-EXTENSION#718. -
Billing changes are not in the current PR head: The current branch’s
billing-chat-gating.test.tsremains unchanged. A separate commit (9c8fadbc) contains the billing-test improvements, but it is not part of PR#1118’s commit list or diff. -
Blocking redaction issue: Both canonical and standalone fallback helpers call
str(text)outside a guard. An object whose__str__raises can therefore make the exception handler fail, violating the documented “never raises” contract. Two unresolved inline review comments identify this atproxy.py:114andyoutube_proxy.py:122. -
Unchanged raw-error paths:
speech_to_text_service.pyandtranscript_action_workflow.pystill log/return raw exception strings in download and transcription failure paths. The PR explicitly excludes Python-APIYoutubeDLcall sites because no proxy echo was demonstrated; this assumption should be confirmed with regression tests. -
Minor regex edge case: An unencoded
@in password text can leave part of the credential unmasked; the existing reviewer classified this as non-blocking because it is URL-invalid and Webshare credentials are reportedly alphanumeric. -
CI status: Security/lint/build checks are passing, but
test,trivy, and coverage were still in progress;Agent completion enforcementwas failing and the PR status remained pending.
🔇 Additional comments (4)
shared/libs/youtube_proxy.py (1)
15-15: LGTM!Also applies to: 43-80, 96-100
src/youtube_extension/utils/proxy.py (1)
16-16: LGTM!Also applies to: 32-72
src/youtube_extension/backend/enhanced_video_processor.py (1)
28-32: LGTM!src/youtube_extension/backend/services/youtube/adapters/robust.py (1)
23-27: LGTM!
Both redaction helpers documented a "never raises" contract but called str(text) unguarded. Since the helper runs inside except blocks, an object whose __str__ itself raises would propagate out of the sanitizer, masking the original failure and suppressing the sanitized log/response. Wrap the coercion in try/except and fall back to a fixed, non-sensitive "<unprintable error>" placeholder on failure, in both the canonical helper (src/youtube_extension/utils/proxy.py) and the standalone fallback (shared/libs/youtube_proxy.py). Add a regression test exercising an object with a raising __str__. Addresses CodeRabbit (critical) and Copilot review findings on PR #1118. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PTuvfPb1mbuCq7CRK5v2zS
|
@coderabbitai review Both review findings — the unguarded Generated by Claude Code |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- Commit ---'
git show --no-ext-diff --format=fuller --stat 3076bad
echo
echo '--- Modified Python file outlines ---'
for f in src/youtube_extension/utils/proxy.py shared/libs/youtube_proxy.py; do
if [ -f "$f" ]; then
echo "### $f"
ast-grep outline "$f" --items all --type function --match 'redact_proxy_credentials|test_object_with_raising_str_returns_placeholder' || true
fi
done
echo
echo '--- Guarded coercion implementations ---'
rg -n -A45 -B8 'def redact_proxy_credentials\s*\(' \
src/youtube_extension/utils/proxy.py shared/libs/youtube_proxy.py
echo
echo '--- Regression test and references ---'
rg -n -A35 -B8 'def test_object_with_raising_str_returns_placeholder\s*\(|class .*Raising|__str__\s*\(' \
-g '*.py' .Length of output: 125216 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- Exact commit diff for relevant files ---'
git diff --no-ext-diff 3076bad^ 3076bad -- \
src/youtube_extension/utils/proxy.py \
shared/libs/youtube_proxy.py \
tests/unit/test_proxy_utils.py
echo
echo '--- Standalone fallback implementation and caller ---'
rg -n -A65 -B10 'def _redact_proxy_credentials\s*\(|def redact_proxy_credentials\s*\(|_redact_proxy_credentials\s*\(' \
shared/libs/youtube_proxy.py
echo
echo '--- Tests for standalone/fallback loading and redaction ---'
rg -n -i -A35 -B8 \
'youtube_proxy|standalone|fallback|_redact_proxy_credentials|unprintable error|raising_str' \
-g '*.py' tests shared srcLength of output: 1093 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- Fallback redaction helper ---'
sed -n '85,185p' shared/libs/youtube_proxy.py
echo
echo '--- Fallback export and delegation paths ---'
rg -n -A20 -B10 '_redact_proxy_credentials|redact_proxy_credentials' \
shared/libs/youtube_proxy.py
echo
echo '--- Direct tests of the fallback module ---'
rg -n -i -A35 -B8 \
'shared\.libs\.youtube_proxy|youtube_proxy\.py|youtube_proxy|spec_from_file_location|SourceFileLoader|importlib\.util' \
-g 'test_*.py' testsLength of output: 54313
The two reported findings are addressed. You are interacting with an AI system. |
Review findings resolved — head
|
| finding | status |
|---|---|
🔴 unguarded str(text) in proxy.py |
fixed (3076bad5f, kept) |
🔴 same in shared/libs/youtube_proxy.py |
fixed (3076bad5f, kept) |
| 🟠 regex: empty user + query over-redaction | fixed (3c80dd4be, kept) |
| unguarded redaction transformation | fixed in 14e2f29d4 |
| 3 call sites stringifying outside the guard | fixed in 14e2f29d4 |
The finding the reviews didn't catch
Guarding str(text) inside the helper isn't sufficient, because three call sites in shared/libs/youtube_proxy.py (lines 418/425/431) were calling:
_redact_proxy_credentials(str(error)) # ← str() evaluated BEFORE the guard is enteredFor those three sites — the actual production leak paths in the standalone-loaded duplicate — a hostile __str__ still raised before the helper ran, so the guard was dead code there. Now they pass the object through: _redact_proxy_credentials(error).
I also guarded the redaction transformation itself. It calls re.sub and os.getenv; if that machinery fails, the pre-existing code would have propagated. It now fails closed, returning <redaction failed> rather than the unvouched original — returning the original would risk emitting the very credential being stripped. (The PR body's ## Risk section previously described the helper as "fail-open"; that was inaccurate and has been corrected.)
Verification
test_proxy_utils.py+ both sink suites → 206 passed- The 5 new never-raises tests run against the pre-fix helper via
git stash→ all 5 fail; post-fix 0 fail. Non-vacuous. - Full
tests/unitnode-ID delta → 0 regressions (264 pre-existing failures unchanged) ruffdiffed against theorigin/mainbaseline on the changed files → no new findings (10 vs 10; the remainder is pre-existingUP006/F841debt inshared/)- Standalone
importlibload path re-verified across 7 inputs incl. hostile__str__/__repr__— all safe, path-@still preserved
One process note: redaction output was verified with len() and hex comparison rather than visual inspection — terminal rendering of \* sequences is actively misleading here (a 28-character output displayed as 19 characters during this work).
Housekeeping
#1120 targeted the same canonical issue and tripped .github/workflows/pr-governance.yml's one-PR-per-issue gate. Its head 3076bad5f is an ancestor of this head, so this PR is a strict superset; #1120 is closed with the containment proof and both of its commits are preserved here with authorship intact.
|
Carrying over a security finding that landed on the now-closed #1120 (so it isn't lost), scoped precisely to what this PR does not already cover. Finding: in
Note these are in Reachability, honestly split:
Suggested (trivial, matches the sibling method): wrap each with the already-in-scope helper, e.g. Entirely your call on whether it meets your "demonstrated leak ⇒ fix" bar — surfacing it because it's an in-file inconsistency that would otherwise vanish with the closed duplicate, not to reopen scope. No change pushed. Generated by Claude Code |
RFC 3986 requires "@" inside userinfo to be percent-encoded, but real *_PROXY values are routinely set with a raw "@" in the password. Excluding "@" from the user/password character classes made the match terminate at the FIRST "@", so the password tail survived into the log line: in: HTTPS_PROXY=http://user:pa@ss@proxy.internal:8080 out: http://***:***@ss@proxy.internal:8080 <-- "ss" leaked Permit "@" inside both classes. The classes remain bounded by the authority delimiters (\s, /, ?, #), so greedy backtracking now settles on the LAST "@" within a single authority rather than the first, while: - \s exclusion stops a match spanning two space-separated URLs; - / exclusion stops a match spanning comma-separated URLs (the following "http://" contains "/") and preserves paths such as example.com/a@b; - ?/# exclusion preserves "@" in query strings and fragments. The mandatory literal "@" before the host still prevents a bare "host:port" from being mistaken for "user:password". Applied to the canonical helper and to the drifted standalone copy in shared/libs/ so the importlib fallback path is covered too. Adds 5 regression tests; the 2 leak assertions fail against the previous regex, the 3 over-redaction guards pin the preservation behaviour. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Correct the #1118 claim: it is CONFLICTING/DIRTY (needs rebase), under security review, possibly closeable as obsolete — not green, not fast-trackable. - Replace fast-track group A with an evaluate-after-rebase methodology: green/red is not a usable signal until PRs are rebased past the #1151/#1142 gate fixes; require >=1 green required check (build/test/Coverage/validate-gh-aw). - Strengthen the loop finding (46/50 open PRs are drafts; generation-rate problem) and adopt the owner's preferred remediation: write to an issue/workflow summary, pause until drained. Does not auto-close #1044/#1059 (owner's call). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016hdiTXBJUUCgw9QX23tp6G
Security review: fix is correct — one test defect blocksVulnerability confirmed live on Real sinks traced (not theoretical):
The two-pass Completeness verified: only two files ever place Blocking
Non-blockingA password containing an unencoded Verdict: merge after rebase + test fix. Not obsolete — the vulnerability is still present on current |
Closing — branch orphaned by the secret-purge force-push
This is unlanded security work and should be re-cut promptly. Nothing matching proxy-credential redaction appears in Tracked as a checked item in #1378. Branch retained for archive-tagging so the original diff stays recoverable as a reference when re-cutting. Generated by Claude Code |
…a dependency (#1438) * fix(security): redact proxy credentials that do not match the env value 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 * fix(deps): drop phantom python-jose to clear the unfixable ecdsa advisory 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 * fix(deps): generate PyJWT auth instead of python-jose 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 --------- Co-authored-by: Claude <noreply@anthropic.com>
Canonical issue
Closes #1113
Outcome
Proxy credentials configured via
WEBSHARE_PROXY_URL/HTTP(S)_PROXYcan no longer reach a log sink or an HTTP response body. Before this change, a single failedyt-dlpinvocation was enough to write a plaintext proxy password into application logs and return it to an API caller. After it, every path that can carry that string is redacted at the boundary, and malformed proxy configuration is contained instead of raising.Scope
utils/proxy.py(the canonical helper), the two concrete leak sites (enhanced_video_processor.py,adapters/robust.py), the drifted duplicate inshared/libs/youtube_proxy.py, and regression tests.yt_dlp.YoutubeDL(ydl_opts)Python-API call sites (video_processing_service.py:259,transcript_action_workflow.py:1033,speech_to_text_service.py:229,video_processor_factory.py:112). Those pass the proxy inside a dict rather than on argv, soDownloadErrordoes not reliably echo it — no demonstrated leak, so no speculative change.Risk
exceptblocks, on paths that were already failing, and it now honours its "never raises" contract structurally: stringification and the redaction passes are each guarded independently. When redaction machinery itself fails it fails closed, returning a fixed placeholder (<redaction failed>) rather than the unvouched original — returning the original would risk emitting the very credential the function exists to strip.host:portand masks only the userinfo, so operators keep the routing information they need. A test asserts a legitimate path containing@(https://example.com/a@b) is not redacted.exceptblocks, an argument whose__str__raises used to throw a secondary exception that replaced the original — no log line, no response body, and the operator loses the real failure. Guarded in both helpers, and the threeshared/libs/youtube_proxy.pycall sites that stringified outside the guard (_redact_proxy_credentials(str(error))) were fixed to pass the object through.str(e). No schema, config, dependency, or lockfile movement.Verification
Verified on head
370f7cd3cin a clean worktree offmain(abd93326b):pytest tests/unit/test_proxy_utils.py tests/unit/test_enhanced_video_processor.py tests/unit/test_robust_youtube_service.py→ 211 passed.test_proxy_utils.pywere run against pre-fix code viagit stash: 16 fail. They fail for the right reasons (ValueError: Invalid IPv6 URLescapingget_proxy_url; the literal password present inredact_proxy_credentialsoutput). Post-fix: 0 fail. The tests are therefore not vacuous — see billing-chat-gating free-tier test depends on ambient AI_GATEWAY_API_KEY and passes vacuously in CI #1116 for why that distinction matters here.pytest tests/unitnode-ID diff against a stashed baseline: 0 new failures, 16 fixed. Re-run on the final head: 0 regressions, 264 pre-existing failures unchanged.git stash: all 5 fail; post-fix 0 fail. Hostile__str__/__repr__objects, a forced redaction failure, and the no-proxy-configured path are all covered.@in password (review round 2) — reviewers found that excluding@from both userinfo classes made the generic sweep stop at the first@, so the password tail survived:http://user:pa@ss@proxy.internal:8080→http://***:***@ss@proxy.internal:8080. Reproduced (hex-verified), then fixed by permitting@inside the classes so greedy backtracking settles on the last@within one authority. The\s///?/#exclusions still bound the match, so it cannot span two space- or comma-separated URLs, and@in paths, query strings and fragments is preserved. 5 tests added: the 2 leak assertions fail against the pre-fix regex, the 3 over-redaction guards pin the preservation behaviour. Applied to theshared/copy and re-checked through the standaloneimportlibpath.len()and hex, never by eye — terminal rendering of\*sequences is misleading (an output of length 28 displayed as 19 characters during this work).build,test,lint-python,bandit,python-safety,gitleaks,guards,validate,npm-audit,dependency-reviewon this head.ruffdiffed against a pre-change baseline on the same files: no new findings (one transientUP037from a quoted"re.Match[str]"annotation was found and removed).black --checkclean.shared/libs/youtube_proxy.pyis loaded two ways: normally asshared.libs.youtube_proxy, and standalone viaimportlib.util.spec_from_file_locationinsrc/agents/mcp_enhanced_video_processor.py:24-45. Both paths exercised;python3 -m compileallclean.Reproducing the leak on
mainOn
mainthat string reaches bothlogger.warning(f"OpenAI Whisper failed: {e}")(CWE-532) and the returned{'error': str(e)}payload (CWE-209). On this branch both renderhttp://***:***@proxy.example.com:8080.Production evidence
Not applicable as a deployed artifact: this branch changes only Python backend modules and tests. It touches no
apps/webfile, no route, no dependency, and no lockfile, so the Vercel preview build for this head is byte-identical tomain— a preview URL would demonstrate nothing about the change.The production-relevant evidence is behavioural and is captured above: the leak is reproducible on
main(16 tests fail there) and is closed on this head (0 fail), across all three sinks — the log line, the API response body, and the yt-dlp stderr echo.gitleaksandbanditpass on this head.Detail
Three sinks, one root cause
subprocess.CalledProcessError.__str__()renders the full argv. Because the proxy URL is passed as--proxy <url>on the command line, any non-zero exit embeds the credential in the exception text.subprocess.TimeoutExpiredbehaves the same way, andyt-dlpadditionally echoes the proxy back on stderr.enhanced_video_processor.pywhisper fallbacklogger.warningand returned{'error': ...}adapters/robust.py:147logger.warningon yt-dlp failureadapters/robust.py:178raise Exception(... result.stderr)urlparseraises in two distinct placesThe docstring promised "malformed ⇒ return
None", but neither raise site was guarded, so the contract was violated and the exception escaped to callers that log it. Confirmed empirically:http://u:p@[::1urlparse()—Invalid IPv6 URLhttp://u:p@host:notaport.port—Port could not be cast to integerhttp://u:p@host:99999.port—Port out of range 0-65535Both are now wrapped. The warning emitted on the malformed path no longer includes the URL.
Why redaction is two-pass
host:port. Keeps the message useful for triage.(?P<scheme>[A-Za-z][A-Za-z0-9+.\-]*://)(?P<user>[^\s/:@]+)(?::(?P<password>[^\s/@]*))?@.Neither pass is sufficient alone. Exact-only misses yt-dlp's normalized/percent-encoded stderr echo and any other proxy var (
HTTPS_PROXY); generic-only discards the host. Excluding/from the user character class is what prevents over-redacting legitimate URLs with@in the path.The
shared/duplicateshared/libs/youtube_proxy.pycarried an unhardened copy of the same logic. It now delegates to the canonical helper when importable, with an equivalent local fallback for the standalone-importlib load path — the pattern already used bysrc/agents/gemini_video_master_agent.py:60-64. This also fixes a latentUnboundLocalErrorthere:redactedwas only assigned insideif parsed.port:, so a credentialed proxy URL without an explicit port would have crashed the redactor itself.Note on the superseded duplicate (#1120)
While review feedback was being addressed, a second agent pushed two commits to this branch and opened #1120 against the same canonical issue, which tripped the governance gate (
Issue #1113 already has another open implementation PR). Both commits were kept — this branch was rebased onto them, so authorship is preserved:3076bad5fstr(text)coercion in both helpers3c80dd4behttp://:pass@host), exclude?/#from userinfo classes14e2f29d4|
370f7cd3c| this PR | permit@inside the userinfo classes so a raw@in a password no longer leaks its tail; 5 regression tests |3076bad5fis an ancestor of14e2f29d4, so #1118 is a strict superset of #1120 and nothing was lost. #1120 was closed as a duplicate with the containment proof.One conflict-resolution note: the placeholder constants were aligned to the literals asserted by the incoming test (
<unprintable error>,<redaction failed>) so that test passes verbatim.Note on
Agent completion enforcementThis check reports
missing_trusted_publication. It is repo-wide and pre-existing: it requires a check run namedAgent Lock trusted publicationpublished by a trusted GitHub App that is not currently publishing. PRs #1108, #1103 and #1098 show the same failure and were merged regardless. Nothing in this PR affects it.