Skip to content

Add opt-in Referer allowlist and rate limit to the sample server - #53

Open
devin-ai-integration[bot] wants to merge 1 commit into
masterfrom
devin/1788719323-server-hotlink-controls
Open

Add opt-in Referer allowlist and rate limit to the sample server#53
devin-ai-integration[bot] wants to merge 1 commit into
masterfrom
devin/1788719323-server-hotlink-controls

Conversation

@devin-ai-integration

Copy link
Copy Markdown

Summary

Related to #15 (and the demo overload from #7). The sample server only had the undocumented HEADER_CHECK gate, which does nothing against <img src="https://server/?geojson=..."> hotlinking. This adds two more opt-in env-var gates in src/server.js, all off by default (no behaviour change unless set), and documents the three of them in a new README "Server configuration (access control)" table.

  • ALLOWED_REFERERS — comma separated Referer hostname allowlist (nginx valid_referers flavour):
    • example.com exact host; *.example.com matches the apex and subdomains; entries may be full URLs (only the hostname is kept); case-insensitive
    • none allows requests with no Referer (curl / server-to-server). Without it, a missing or unparseable Referer is rejected
    • fails with 403 + ALLOWED_REFERERS_FAIL_MESSAGE
  • RATE_LIMIT_MAX / RATE_LIMIT_WINDOW (seconds, default 60) — fixed-window in-memory counter per client IP (createRateLimiter), no new dependency. Fails with 429 + Retry-After + RATE_LIMIT_FAIL_MESSAGE. Client IP = first X-Forwarded-For entry, else remoteAddress (same source the existing log line already used; README warns to only trust it behind a proxy).

Middleware order: HEADER_CHECK (unchanged, still applies to /health) → /health short-circuit → rate limit → referer check → existing log line. /health is exempt from the two new gates so Docker HEALTHCHECK keeps working.

To make this testable the express app construction moved into an exported createApp(env = process.env); the default export is still createServer(createApp()), so osmsm serve / import server from './server.js' are unaffected.

ALLOWED_REFERERS='none,*.mysite.com' RATE_LIMIT_MAX=30 osmsm serve

Tests: new test/server.test.js (21 cases) boots createApp(env) on an ephemeral port and hits /dynamic (template render, no browser) covering defaults, HEADER_CHECK, all ALLOWED_REFERERS rules, and rate limiting incl. window reset with fake timers. npm test passes (37 tests) on Node 22.12. Also smoke-tested osmsm serve with ALLOWED_REFERERS=example.com: /health 200, no-referer request 403, allowed referer renders a PNG.

CHANGELOG has an Unreleased entry.

Link to Devin session: https://app.devin.ai/sessions/358b3009bd1845f3ac40c3b8176e22b7
Open in Devin Desktop: https://app.devin.ai/desktop/session/358b3009bd1845f3ac40c3b8176e22b7?variant=devin
Requested by: @jperelli

Related to #15

Co-Authored-By: Julian Perelli <jperelli@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration

Copy link
Copy Markdown
Author
Real-browser hotlink check — all five scenarios passed

Tested the local sample server with genuine cross-origin <img> embedding (fixture pages on localhost:8081 and 127.0.0.1:8080) and passive browser network capture.

  • ALLOWED_REFERERS=localhost: page on localhost:8081 rendered the 300×200 map (200).
  • page on 127.0.0.1:8080 showed a broken image: 403 with body Forbidden, hotlinking from this site is not allowed.
  • RATE_LIMIT_MAX=5: sixth request returned 429 with Retry-After: 17; subsequent reloads stayed blocked with a decreasing Retry-After.
  • direct /health returned 200 without a Referer while the quota was still exhausted.
  • regression: restarting with no gate env vars, the 127.0.0.1 page rendered the map again (200).
Denied hostname — gates enabled Regression — gates disabled
127.0.0.1 blocked with gates enabled 127.0.0.1 renders with default configuration

Not exercised in the browser run (covered by test/server.test.js instead): wildcard/apex matching, none, custom messages/windows, per-IP buckets.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant