Skip to content

proxy_pool: refuse a bad URL instead of crashing, and never leak it - #9

Merged
jehrr merged 1 commit into
mainfrom
fix/proxy-url-validation
Sep 9, 2026
Merged

proxy_pool: refuse a bad URL instead of crashing, and never leak it#9
jehrr merged 1 commit into
mainfrom
fix/proxy-url-validation

Conversation

@jehrr

@jehrr jehrr commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

The mediamarkt canary's first run with a real proxy secret crashed, and printed a live proxy login and password into its own CI log doing it. Three defects, all in proxy_pool.py, all triggered by one wrong value.

A malformed URL crashed the run instead of being refused

parse_proxy_line validated the scheme and the host but never the port — and urlparse computes a port lazily, raising ValueError only when something finally asks. So a bad entry passed validation and blew up several calls later inside to_playwright: exit 1 (crash) where it should have been exit 2 (bad usage), with a traceback and no message naming the problem.

The value that caused it is the mistake a new user makes:

proxy list file:  scheme://host:port:login:password    ← what was pasted
a proxy URL:      http://login:password@host:port      ← what is wanted

The extra colons become part of the port. The refusal now says exactly that.

mask() raised on the values that most needed masking

It read parsed.port too. So the one function standing between a password and a log blew up on a malformed URL, and the caller printed the raw string instead. That is how the credential reached the log.

A masker must never raise — it is called precisely when something is already wrong with the value. It is now total: anything it cannot take apart is redacted whole rather than echoed, and every ProxyError message reports mask(line) rather than the line.

A pool of N identical entries claimed to be N exits

A copied proxy list is often one address repeated; the one in use here was fifty identical lines. The pool reported exit 2/50 on every rotation while every one of them left from the same place, and the single-exit warning never fired because it counted entries rather than addresses. A run believed it was spread across fifty exits while burning one.

Duplicates are now dropped, order preserved, and the collapse is logged — a pool quietly smaller than the file that produced it is the same kind of surprise in the other direction.

Also removed

A test asserting that "http://host:port:login:pass" "is understood". It checked only that parse_proxy_line did not reject the line — which it did not, returning it unchanged — so the check passed while the value was never usable.

A test that asserts a function did not complain is not a test that its answer was right. Replaced with one that pins the refusal, the message, and the absence of the credential from it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wp7ctnpMGb5gi67Dco7C5V

A canary run with a real proxy secret crashed and printed a live proxy login
and password into its own CI log. Three defects, all here, all triggered by
one wrong value.

A malformed URL crashed the run instead of being refused
---------------------------------------------------------
parse_proxy_line validated the scheme and the host but never the PORT — and
urlparse computes a port lazily, raising ValueError only when something
finally asks for one. So a bad entry passed validation and blew up several
calls later inside to_playwright: exit 1, a crash, where it should have been
exit 2, bad usage, with a message naming the problem.

The value that caused it is the mistake a new user makes: a line from a proxy
LIST FILE (scheme://host:port:login:password) pasted where a proxy URL
(http://login:password@host:port) belongs. The extra colons become part of
the port. The refusal now says exactly that.

mask() raised on the values that most needed masking
-----------------------------------------------------
It read parsed.port too. So the one function standing between a password and
a log blew up on a malformed URL, and the caller printed the raw string
instead. That is how the credential reached the log.

A masker must never raise. It is called precisely when something is already
wrong with the value. It is now total: anything it cannot take apart is
redacted whole rather than echoed, and every ProxyError message reports
mask(line) rather than the line.

A pool of N identical entries claimed to be N exits
-----------------------------------------------------
A copied proxy list is often one address repeated — the one in use here was
fifty identical lines. The pool reported "exit 2/50" on every rotation while
every one of them left from the same place, and the single-exit warning never
fired because it counted entries rather than addresses. So a run believed it
was spread over fifty exits while burning one.

Duplicates are now dropped, order preserved, and the collapse is LOGGED: a
pool quietly smaller than the file that produced it is the same kind of
surprise in the other direction.

Also removed: a test asserting that "http://host:port:login:pass" "is
understood". It checked only that parse_proxy_line did not reject the line —
which it did not, returning it unchanged — so it passed while the value was
never usable. A test that asserts a function did not complain is not a test
that its answer was right.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp7ctnpMGb5gi67Dco7C5V
@jehrr
jehrr merged commit 63a3e69 into main Sep 9, 2026
6 checks passed
@jehrr
jehrr deleted the fix/proxy-url-validation branch September 9, 2026 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant