Security Testing
Last updated
The offline security suite — SSRF net, filesystem/database hardening, and the fakes that power it.
On this page
Where comic-dl’s hardening lives and how it is tested. The security suite
(tests/security/) is the security spec: every behavior below is encoded as a
failing-if-regressed test. Touch one of these modules and the matching test
must change in the same commit (code review bounces it
otherwise).
Threat model
The user hands the program URLs; untrusted responses (pages, images, XML, redirect chains) flow from there into the application. The suite assumes every input is hostile and proves the code stays inside three boundaries: network, filesystem/database, and parser resilience. Every test is offline — it exercises the guards against a loopback fake host.
Running the suite
uv run pytest tests/security/ -q
./scripts/test.sh # whole suite, includes security
uv run pytest tests/security/ --cov=comic_dl --cov-report=term-missing
Network safety
tests/security/test_network_safety.py is the SSRF net:
- Hostile URL rejection — URLs resolving to loopback, private, link-local,
or metadata addresses (
127.0.0.1,10.0.0.0/8,169.254.169.254,::1, IPv4-mapped::ffff:127.0.0.1) are blocked before any socket opens. - Pre-connect validation — the guard runs before a connection, not as part of retry handling.
- Redirect re-validation — every hop is re-checked; a loopback hop introduced mid-chain aborts the fetch.
- Redirect budget — chains capped at
MAX_REDIRECTS(5). - Scrape-path validation —
BaseScraper._timeout_getrejects private URLs before requesting; redirect chains through127.0.0.1abort. - Scraper entry validation — site scrapers validate initial URLs and resolve relative links to absolute before fetch.
Known limitation: DNS rebinding (TOCTOU)
The guard resolves hostnames at validation time; curl_cffi resolves again
at connect time. A server answering with a public address during validation
and a private address moments later can bridge the check-then-use gap. This is
inherent to validating ahead of the transport, accepted because targets are
user-chosen scrape sites and every redirect hop is re-validated. Closing it
means pinning resolved IPs into the connection itself.
Filesystem, database, and parser hardening
tests/security/test_filesystem_db_parser.py covers:
- Path containment —
sanitize_filenamenever yields an absolute path or..traversal; a hostileImageItem.filenamecannot escape the output dir. - SQL parameterization — every Library query uses bound parameters; quote- or wildcard-laden titles are data, never SQL.
- Schema migration safety — a DB with a future
user_versionis refused, not migrated or corrupted. - Untrusted XML — ComicInfo.xml parses with
defusedxml: XXE and billion-laughs payloads rejected. - Concurrency bounds —
--concurrencyaboveMAX_CONCURRENCYclamps; below 1 is a hard error.
How the tests fake the world
tests/security/_server.pyprovidesFakeHttpServer(async loopback server with routes for plain responses, hostile redirect chains, and mid-stream failures) andNetHttpClient/NetResponse— a minimalcurl_cffi-shaped client mimickingAsyncSession.stream(), so the downloader’s real guarded code paths run end to end without the internet.
Where the guards live
| Guard | Implementation |
|---|---|
| SSRF validation | comic_dl/utils.py (validate_request_url) |
| Downloader streaming + resume | comic_dl/downloader.py (_open_stream, _try_resume) |
| Scrape-path validation + cap | comic_dl/scrapers/base.py (_timeout_get, capped redirects) |
| Concurrency clamp | comic_dl/cli/__init__.py |
| Untrusted XML | defusedxml |
What is deliberately not covered
- Rate limiting hardening — availability/abuse of third-party sites from
polite batch downloading is out of scope (see
SECURITY.md). The politeness layer (rate.py, shared cooldown) is tested for behavior, not adversarial throughput. - Live-site integration — no test depends on an external site being up; adding one is a review failure, not an improvement.