fix(server): refuse cross-site requests on the terrain-heights route

Follow-up to #686 requested on #923. The route's per-client limiter keys on the socket address, which for a cross-site <img> is the visitor's own browser, so any web page could fill the bounded cache with junk keys and spend the visitor's 90/min quota, leaving the app's own terrain lookups on 429. Gate the route with admitSameSite before the limiter, like the Places routes.

Co-authored-by: daikaginza <[email protected]>
This commit is contained in:
Sunilanddaikaginza committed 2026-10-08 21:13:16 +05:30
1 parent bea76df454
commit ff799d1520
4 files changed
+58 -4

No files matched your search

+8 -1
View File
@@ -9,6 +9,7 @@ import {
validTerrainResult,
} from '../../src/data/terrainHeightsProxy.js';
import { makeRateLimiter, clientKey } from './common/rate-limit.js';
import { admitSameSite } from './common/same-site.js';
/**
* Re:Earth terrain point-height proxy: batched lon/lat → ellipsoidal height
@@ -25,7 +26,9 @@ import { makeRateLimiter, clientKey } from './common/rate-limit.js';
* Bounds, because every distinct 5dp point is its own key: coordinates must be
* in WGS-84 range (rejected at parse), the point cache has an entry ceiling
* with oldest-first eviction, an over-large cache file is not read back, and
* the route carries an always-on per-client limiter.
* the route carries an always-on per-client limiter. The route also refuses
* cross-site browser requests, so another web page cannot use the visitor's
* browser to fill the cache or spend the visitor's own rate-limit quota.
*/
export function terrainHeightsProxy(options = {}) {
// Destructured in the body, not the signature: `proxyErrorResponses.test.mjs`
@@ -231,6 +234,10 @@ export function terrainHeightsProxy(options = {}) {
const installMiddleware = (server) => {
server.middlewares.use('/api/terrain/heights', async (req, res) => {
// Refuse cross-site browser requests (an <img> or fetch from another
// site) before the limiter, cache or upstream see them. Same-origin app
// requests and non-browser loopback callers (MCP panel_request) pass.
if (admitSameSite(req, res)) return;
const send = (status, bodyObj) => {
if (res.headersSent) return;
res.writeHead(status, { 'Content-Type': 'application/json' });