-
Notifications
You must be signed in to change notification settings - Fork 0
Security: harden tile proxy against third-party abuse #204
Description
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
- Verify embed tile loading — do embeds currently work? Does the Referer check break them? Test
/Public/Users/Timeline/{user}/embedand/Public/Trips/{id}?embed=trueinside an iframe on a different domain - 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
- 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)
- Per-embed rate limiting — rate limit by embed token rather than (or in addition to) IP
- Sliding window rate limiter — replace fixed-window to prevent boundary batching
- Outbound request budget — cap upstream OSM requests per time window to prevent cascade abuse regardless of source
- Security headers — add X-Content-Type-Options: nosniff, CSP. X-Frame-Options must be carefully configured to allow legitimate embeds while preventing clickjacking
- 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 applicationAreas/Public/Controllers/UsersTimelineController.cs— timeline + embed endpointsAreas/Public/Controllers/TripViewerController.cs— public trip viewerAreas/Public/Views/UsersTimeline/Embed.cshtml— timeline embed viewViews/Shared/_EmbedLayout.cshtml— minimal embed layout (no navbar/footer)Services/RateLimitHelper.cs— fixed-window per-IP rate limiterServices/TileCacheService.cs— tile caching, upstream requestsModels/ApplicationSettings.cs— rate limit config (TileRateLimitEnabled, TileRateLimitPerMinute)Views/Shared/_Layout.cshtml— injectswindow.wayfarerTileConfigwith tile URL (line ~203)Program.cs— ForwardedHeaders, HttpClient config, middleware pipeline
References
- OSM tile usage policy: https://operations.osmfoundation.org/policies/tiles/
- Tile cache compliance: OSM tile usage policy: implement conditional requests and fix canonical URL #201, OSM tile policy: conditional requests, cache headers, canonical URL #202
- Original 403 investigation: [Bug]: Access Blocked OSM tile access #199