Skip to content

security/robustness: harden annotator CourtListener client (host-pinning, shared retry, response cap) #223

Description

@williamzujkowski

Found alongside the YAML-escaping fix (separate PR). Three lower-severity hardening gaps in @civic-source/annotator's CourtListener client, each adjacent to an already-shipped fix in another package but not present here.

1. HttpUrlSchema does not pin host — absolute_url can redirect off-domain (MEDIUM)

annotator.ts:131 builds sourceUrl = \https://www.courtlistener.com${result.absolute_url}\`` from untrusted API data. absolute_url = '@evil.com/x'https://www.courtlistener.com@evil.com/x (authority evil.com); .evil.com/x...courtlistener.com.evil.com/x. Both pass HttpUrlSchema (#210 only blocks non-http(s) schemes, not host). These land in sourceUrl and are later rendered as hrefs, so a poisoned field becomes an off-site link in published annotations.
Fix: parse with new URL(absolute_url, base) and assert hostname === 'www.courtlistener.com', or require absolute_url to match /^\/[\w/-]*$/. (Mirrors the #201/#205 fetcher host-pinning.)

2. Client reimplements retry, bypassing shared hardening; no abort/timeout (MEDIUM)

client.ts:88-132 defines a private fetchWithRetry instead of using @civic-source/shared's, so it does NOT inherit #212/#213: it ignores Retry-After on 429 (fixed backoff only), and fetch is called with no signal/timeout, so a hung connection blocks searchByStatute indefinitely.
Fix: use the shared fetchWithRetry(url, { headers, signal, logger }); keep only the annotator-specific 401 mapping.

3. No response size cap on API body (LOW)

client.ts:97 calls await response.json() with no Content-Length check or streaming cap; a misbehaving/redirected endpoint can return an unbounded body and OOM the importer. (Same class as the fetcher's #201/#205 cap.)
Fix: check content-length against a few-MB max before .json(), or read with a capped reader.

Notes

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions