Skip to content

Security: harden tile proxy against third-party abuse #204

@stef-k

Description

@stef-k

Context

The tile proxy endpoint (/Public/tiles/{z}/{x}/{y}.png) serves cached OSM tiles to the application and all public-facing map features. During the v1.2.17 work (#201, #202) an audit of the tile endpoint security surface was performed, revealing gaps in abuse prevention — particularly important given that the application provides embeddable maps for external use.

Public features that load tiles from the proxy

All of these use Leaflet with the tile URL from window.wayfarerTileConfig:

Feature URL Embed-designed? Notes
Public Timeline /Public/Users/Timeline/{username} No (full page) User must have IsTimelinePublic enabled
Public Timeline Embed /Public/Users/Timeline/{username}/embed Yes_EmbedLayout.cshtml, no chrome Designed for iframes in external blogs/sites
Public Trip Viewer /Public/Trips/{id} Yes — via ?embed=true Trip must be marked IsPublic

The embed problem

When a user embeds a timeline or trip map in blog.example.com, the browser sends tile requests with Referer: https://blog.example.com/.... The current Referer check in TilesController.IsValidReferer() compares the Referer host against Request.Host (the application host) — this will reject tile requests from legitimate embeds.

This needs immediate investigation: either embeds are already broken and nobody noticed, or there's a code path that bypasses the Referer check for embeds that we haven't identified yet.

Current defenses

Defense What it stops What it doesn't stop
Referer check (same-host match) Browser <img> hotlinking, Leaflet from other origins curl, Python, any non-browser client — header is trivially spoofable. Also blocks legitimate embeds.
Rate limiting (500 req/min per IP, fixed window) Single-IP scraping, naive bots Rotating proxies, botnets, VPNs; also vulnerable to fixed-window boundary batching (500 at :59s + 500 at :00s = 1000 in 2 seconds)
No CORS policy Browser-based cross-origin XHR/fetch Direct HTTP clients, server-side proxies
Cache-Control: public, max-age=86400 Repeated browser requests from same client Nothing server-side
Coordinate validation (z 0-22, x/y bounds) Invalid/out-of-range coordinates Enumeration within valid bounds
Authenticated users bypass rate limiting N/A A compromised or abusive account has unlimited access

What's missing

  • No authentication or token required for tile access — URL pattern is fully predictable and enumerable
  • No distributed rate limiting — memory-based only, ineffective behind load balancers or multiple instances
  • No outbound rate limiting — cache misses cascade directly to upstream OSM, risking our server being blocked by OSM for fair use violation
  • No security headers — missing X-Content-Type-Options, CSP (X-Frame-Options must allow embeds)

Abuse scenarios

1. Casual hotlinking (low effort)

Someone uses L.tileLayer('https://our-site/Public/tiles/{z}/{x}/{y}.png') on their site.
Currently blocked by Referer check in browsers — but this also blocks our own legitimate embeds.

2. Server-side proxy (moderate effort)

A third party proxies tiles through their own backend, spoofing/omitting the Referer.
Not blocked — rate limit applies per their single server IP (500/min is enough for a small site).

3. Distributed scraping (higher effort)

Rotating IPs pre-download tiles. 10 IPs × 500/min = 5000 tiles/min.
Not blocked by current defenses.

4. Upstream cascade risk

Any cache-miss abuse cascades to OSM upstream under our User-Agent. OSM could block our server for fair use violation, affecting all legitimate users.

Areas to investigate

  1. Verify embed tile loading — do embeds currently work? Does the Referer check break them? Test /Public/Users/Timeline/{user}/embed and /Public/Trips/{id}?embed=true inside an iframe on a different domain
  2. Token-based tile URLs — e.g., HMAC-signed URLs with expiry, generated per embed session. The embed view could inject a signed tile URL that the proxy validates. Prevents unauthorized use while allowing legitimate embeds
  3. Embed-aware Referer policy — allow Referer from known embed contexts (e.g., embed endpoints could set a cookie or token that the tile proxy accepts alongside the host check)
  4. Per-embed rate limiting — rate limit by embed token rather than (or in addition to) IP
  5. Sliding window rate limiter — replace fixed-window to prevent boundary batching
  6. Outbound request budget — cap upstream OSM requests per time window to prevent cascade abuse regardless of source
  7. Security headers — add X-Content-Type-Options: nosniff, CSP. X-Frame-Options must be carefully configured to allow legitimate embeds while preventing clickjacking
  8. Monitoring/alerting — tile cache hit ratio, unusual traffic patterns, per-IP request volume

Key files

  • Areas/Public/Controllers/TilesController.cs — tile endpoint, Referer check (IsValidReferer()), rate limit application
  • Areas/Public/Controllers/UsersTimelineController.cs — timeline + embed endpoints
  • Areas/Public/Controllers/TripViewerController.cs — public trip viewer
  • Areas/Public/Views/UsersTimeline/Embed.cshtml — timeline embed view
  • Views/Shared/_EmbedLayout.cshtml — minimal embed layout (no navbar/footer)
  • Services/RateLimitHelper.cs — fixed-window per-IP rate limiter
  • Services/TileCacheService.cs — tile caching, upstream requests
  • Models/ApplicationSettings.cs — rate limit config (TileRateLimitEnabled, TileRateLimitPerMinute)
  • Views/Shared/_Layout.cshtml — injects window.wayfarerTileConfig with tile URL (line ~203)
  • Program.cs — ForwardedHeaders, HttpClient config, middleware pipeline

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions