Loading search…

Explain with AI
Edit this page

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_get rejects private URLs before requesting; redirect chains through 127.0.0.1 abort.
  • 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_filename never yields an absolute path or .. traversal; a hostile ImageItem.filename cannot 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_version is refused, not migrated or corrupted.
  • Untrusted XML — ComicInfo.xml parses with defusedxml: XXE and billion-laughs payloads rejected.
  • Concurrency bounds — --concurrency above MAX_CONCURRENCY clamps; below 1 is a hard error.

How the tests fake the world

  • tests/security/_server.py provides FakeHttpServer (async loopback server with routes for plain responses, hostile redirect chains, and mid-stream failures) and NetHttpClient/NetResponse — a minimal curl_cffi-shaped client mimicking AsyncSession.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.