refactor: simplify masking to dumb mask_url/mask_proxy + add stable_id

The selective-key URL/proxy machinery (_mask_qs, SENSITIVE_QUERY_KEYS,
_split_host, _bullet_trailing_fields) tried to redact tokens inside arbitrary
URL structure and leaked on shapes it didn't anticipate (SPA-route fragments,
colon-passwords). Replace with purpose-honest functions that cannot leak or
over-mask:

- mask_url: strips query + fragment, keeps scheme/host/path. Does not parse or
  hunt for sensitive params - a secret in a URL is the caller's bug.
- mask_proxy: partial user-reportable proxy id - drops any userinfo credentials
  and masks an IPv4 host's middle octets (A.***.***.D, port kept); non-IPv4
  host passes through; no parseable host raises ValueError.
- stable_id: deterministic sha256-derived id from ordered str parts (\x00-joined,
  regenerable); empty/non-str part or non-positive length raises ValueError.

credit/cvv/phantom/provider unchanged. Export stable_id from the facade.

Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
2026-07-06 18:25:06 -04:00
parent e6e655335e
commit 0030daeb7b
3 changed files with 87 additions and 113 deletions
+27 -24
View File
@@ -144,7 +144,7 @@ Display helpers only — they format a value for showing; they are **not a secur
control** (the underlying value is unchanged and still needs proper handling).
```python
from commons import credit, cvv, phantom, provider, mask_url, mask_proxy
from commons import credit, cvv, phantom, provider, mask_url, mask_proxy, stable_id
credit("4111 1111 1111 1234") # "•••• •••• •••• 1234"
cvv("123") # "•••"
@@ -153,32 +153,35 @@ phantom("1234567890") # "••••••••••" (len <= 10
provider("4111111111111111") # "VISA" (VISA/MC/AMEX/UPAY/DISC/JCB/DNRS/UNKW)
provider("4111²111111111234") # "VISA" (unicode digit lookalikes ignored, never raises)
# redact credentials in a connection string before logging it
mask_url("https://u:pw@api.x/v2?apiKey=SECRET&ip=8.8.8.8")
# -> "https://api.x/v2?apiKey=***&ip=8.8.8.8" (userinfo dropped, secret query masked)
mask_url("redis://:pw@127.0.0.1:6379/0") # -> "redis://127.0.0.1:6379/0"
mask_url("https://x/cb#access_token=SECRET&token_type=bearer")
# -> "https://x/cb#access_token=***&token_type=bearer" (oauth implicit-grant fragment masked)
mask_proxy("1.2.3.4:8080:user:supersecret") # -> "1.2.3.4:8080:user:****"
mask_proxy("1.2.3.4:8080") # -> "1.2.3.4:8080" (no auth, untouched)
mask_proxy("user:supersecret@1.2.3.4:8080") # -> "1.2.3.4:8080" (userinfo shape, also masked)
# strip query + fragment off a url before logging it
mask_url("https://u:pw@api.x/v2?apiKey=SECRET&ip=8.8.8.8") # -> "https://u:pw@api.x/v2"
mask_url("https://api.x/v2") # -> "https://api.x/v2" (unchanged)
# partial, user-reportable proxy id: creds stripped, ipv4 middle octets masked
mask_proxy("172.58.32.1:8080") # -> "172.***.***.1:8080"
mask_proxy("user:supersecret@172.58.32.1:8080") # -> "172.***.***.1:8080" (creds gone)
mask_proxy("proxy.host.net:8080") # -> "proxy.host.net:8080" (hostname untouched)
# deterministic, regenerable id from ordered parts
stable_id("profit_lounge", "user_123") # -> "d1f4…" (same 16 hex chars every call)
```
`mask_url` strips `user:pass@` userinfo and replaces the values of sensitive query
params (`apiKey`, `token`, `password`, `secret`, …; override via `keys=`) with `***`.
Non-sensitive query values are re-percent-encoded on the way out, so a value with a
reserved character (`&`, `=`, a space, …) round-trips correctly instead of corrupting
the rebuilt URL. The URL fragment is masked the same way as the query when it is
genuinely `key=value&key=value` shaped (e.g. an OAuth implicit-grant callback) — a
fragment that only incidentally contains `=` (an SPA hash route like `#/page?x=1`) is
left untouched rather than risking a lossy rewrite of a non-secret value, and a plain
anchor (`#section`) always passes through unchanged.
`mask_url` drops the query and fragment, keeping scheme/host/path. It does **not** parse
or hunt for sensitive params — a secret in a URL is the caller's bug, not this function's
to detect — so it structurally cannot leak a query param and cannot over-mask a legit one.
A URL with no query/fragment is returned unchanged. (Note: `user:pass@` userinfo is part of
the authority and is **not** stripped; keep credentials out of the URL you log.)
`mask_proxy` bullets the password of a `host:port:user:password` spec, and a plain
`host:port` (no auth, including a bracketed IPv6 host) passes through unchanged. Any
other credential-bearing shape — `user:pass@host:port` userinfo, a `scheme://`-prefixed
URL, or a colon spec with more than 4 parts — is masked rather than ever returned
verbatim; it never logs a password in the clear.
`mask_proxy` yields a partial identifier a user can safely report for debugging. Any
`user:pass@` userinfo (or a `scheme://` prefix) is dropped entirely, then an IPv4 host
`A.B.C.D` is masked to `A.***.***.D` with the port kept in full; a non-IPv4 host (hostname
or IPv6) passes through unchanged with only its credentials stripped. A spec with no
parseable host raises `ValueError`.
`stable_id` derives a fixed-length hex id from ordered string parts via SHA-256 — same
inputs always produce the same id, no state, so it's regenerable anywhere. Parts join with
a `\x00` separator so `("ab","c")` and `("a","bc")` differ; an empty or non-str part, or a
non-positive `length`, raises `ValueError`.
## retry