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://[email protected]/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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions