Skip to content

Commit 4e149fb

Browse files
sdornanclaude
andcommitted
fix(hltb): redact session material from the request debug log
The debug log printed the session headers and the honeypot pair verbatim. The token decodes to "<issued-at>::<public IP>|<user agent>|<key>|<hmac>", so anyone pasting DEBUG logs into a bug report published their host's public IP address. Logs are also downloadable through /api/logs. Redact the session headers and drop the honeypot entry from the logged payload, keeping the URL, search terms and timeout that make the line useful. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 39d7fa0 commit 4e149fb

2 files changed

Lines changed: 35 additions & 2 deletions

File tree

backend/handler/metadata/hltb_handler.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@
2727
HLTB_RATE_LIMIT_BACKOFF_SECONDS: Final[float] = 2
2828
_rate_limiter = RateLimiter(HLTB_MAX_REQUESTS_PER_SECOND)
2929

30+
# The session token decodes to "<issued-at>::<public IP>|<user agent>|<key>|<hmac>",
31+
# so logging it would put the host's public IP in any shared log or support bundle.
32+
HLTB_SESSION_HEADERS: Final[frozenset[str]] = frozenset(
33+
{"x-auth-token", "x-hp-key", "x-hp-val"}
34+
)
35+
3036

3137
class HLTBPlatform(TypedDict):
3238
slug: str
@@ -332,8 +338,11 @@ async def _request(self, url: str, payload: dict) -> dict:
332338
log.debug(
333339
"HowLongToBeat API request: URL=%s, Headers=%s, Payload=%s, Timeout=%s",
334340
url,
335-
headers,
336-
body,
341+
{
342+
key: "[redacted]" if key in HLTB_SESSION_HEADERS else value
343+
for key, value in headers.items()
344+
},
345+
{key: value for key, value in body.items() if key != self.hp_key},
337346
60,
338347
)
339348

backend/tests/handler/metadata/test_hltb_handler.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,30 @@ async def test_github_endpoint_fetch_is_not_rate_limited(
166166
acquire.assert_not_awaited()
167167

168168

169+
@patch("handler.metadata.hltb_handler.HLTB_API_ENABLED", True)
170+
@patch("handler.metadata.hltb_handler.ctx_httpx_client")
171+
async def test_debug_log_does_not_leak_session_material(mock_ctx_httpx_client):
172+
# Logs are downloadable via /api/logs and routinely pasted into bug reports,
173+
# and the token decodes to a string containing the host's public IP.
174+
handler = _handler()
175+
mock_client = AsyncMock()
176+
mock_client.post.return_value = _response(json_body={"data": []})
177+
mock_ctx_httpx_client.get.return_value = mock_client
178+
179+
with patch("handler.metadata.hltb_handler.log.debug") as mock_debug:
180+
await handler._request(
181+
"https://howlongtobeat.com/api/bleed", {"searchTerms": ["Chrono"]}
182+
)
183+
184+
logged = repr(mock_debug.call_args.args)
185+
for secret in ("token-1", "ign_aaaa", "val-1"):
186+
assert secret not in logged
187+
188+
# The parts that make the log useful are still there.
189+
assert "Chrono" in logged
190+
assert "[redacted]" in logged
191+
192+
169193
@patch("handler.metadata.hltb_handler.HLTB_API_ENABLED", True)
170194
@patch("handler.metadata.hltb_handler.ctx_httpx_client")
171195
async def test_request_backs_off_and_retries_on_429(mock_ctx_httpx_client):

0 commit comments

Comments
 (0)