Skip to content

Constrain the ChapterController catch-all route so unknown paths 404 at the router #2891

Description

@mroderick

The chapter catch-all route (config/routes.rb:206, get ':id' => 'chapter#show', as: :chapter) matches every root-level path that no other route claims, and sends all of it — including scanner junk — into the controller and a database query. Adding a slug-format constraint would 404 the overwhelming majority of that traffic at the router, with zero effect on real chapter pages.

Evidence (2026-09-16, canonical log archive)

In one day, ChapterController served 4,218 requests — 7× more than any other controller. 91% were 404s:

Traffic type Requests Examples
Sensitive-file probes ~2,986 /key.pem, /id_rsa, /rclone.conf, /docker-compose.yml, /secrets.env
Vite RSC CVE probe 250 /__vite_rsc_findSourceMapURL
Targeted probe 142 /z9x8c7v6b5-debug-trigger-codebar.io
Real chapter pages ~370 /shanghai, /nairobi, /brighton, /oslo

The history (found via git archaeology): the catch-all dates to c322c5af (2014, "add local chapter page"), where it was a deliberate product choice — chapter pages at codebar.io/london rather than codebar.io/chapters/london. The URL design is still right; the unconstrained implementation predates the era of mass scanner traffic.

Proposed change

get ':id' => 'chapter#show', as: :chapter, constraints: { id: /\A[a-z0-9-]+\z/ }

Chapter slugs are generated with name.parameterize (app/models/chapter.rb:78), which produces only lowercase letters, digits, and hyphens, so this constraint matches every real slug and nothing else. Paths containing dots, uppercase letters, or other characters — most of the probe traffic — would 404 at the router without touching ChapterController or the database.

Notes

  • This is complementary to PR feat: block scanner junk at the nginx edge #2884 (nginx edge block), which filters the observed probe classes before nginx routing. The constraint is structural: it also covers whatever the edge list misses, such as any path with a dot or uppercase character.
  • The remaining junk that still matches the constraint (e.g. /backup, /rclone) continues to 404 in-app as today; the edge block handles the known ones.
  • Testing: add a routing spec asserting the constraint (real slugs route; dotted/uppercase paths do not match). The nginx CI smoke matrix from feat: block scanner junk at the nginx edge #2884 is unaffected.
Full 2026-09-16 breakdown
  • 4,218 canonical ChapterController#show requests vs 618 for the next controller (EventsController)
  • 3,838 of 4,218 (91%) were 404s
  • ~2,986 sensitive-file probes across 51 distinct paths (SSH keys, cloud configs, .env variants, service-account JSONs, WordPress paths)
  • 250 /__vite_rsc_findSourceMapURL probes (known Vite RSC source-map disclosure CVE scan)
  • 142 /z9x8c7v6b5-debug-trigger-codebar.io targeted probes
  • Sources are distributed (each IP ~4 requests via Cloudflare); bursts of 100–190 req/min
  • Genuine chapter traffic: /shanghai 147, /nairobi 143, /brighton 17, /oslo 11 — all 200s
  • Related: feat: block scanner junk at the nginx edge #2884 adds the nginx edge block and CI smoke tests for the same traffic

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions