Merge pull request #1427 from Sunil56224972/security/cross-site-gate-overpass-military

fix(security): add cross-site gate to military-installations proxy
This commit is contained in:
Sameh Khamis authored and GitHub committed 2026-11-02 07:45:16 -08:00
commit bab5157989
5 files changed
+52 -4

No files matched your search

@@ -0,0 +1,4 @@
- The military-installations proxy (`/api/military-installations`) now refuses
cross-site and reverse-proxy-forwarded browser requests with 403 before any
Overpass work, through the same shared gate as `/api/overpass`. Same-origin
app requests and local tools such as curl still work (@Sunil56224972, #1427).
+2
View File
@@ -5086,6 +5086,8 @@ repointing `OVERPASS_UPSTREAMS` refetches instead of serving another instance's
answers; untagged entries stop serving once an instance is configured.
The shared transport guards `/api/overpass` and `/api/military-installations`.
Both query handlers refuse cross-site and forwarded browser requests through
the shared local-request gate, before any rate-limit, cache or upstream work.
With no configured upstream, each route serves existing cache data (including
expired entries, marked stale with the original cache/retrieval date) without
refresh. An annotation miss returns HTTP 503, `OVERPASS_NOT_CONFIGURED`,
+3 -1
View File
@@ -432,7 +432,9 @@
"server/providers/overpass/transport.js",
"src/sources/httpBody.js",
"src/i18n/marker.js",
"src/sources/rateLimit.js"
"src/sources/rateLimit.js",
"server/providers/common/same-site.js",
"src/localRequestGate.mjs"
],
"external": []
},
@@ -1,4 +1,5 @@
import { makeRateLimiter, clientKey } from './common/rate-limit.js';
import { admitSameSite } from './common/same-site.js';
import {
MILITARY_INSTALLATION_ELEMENT_CAP,
MILITARY_INSTALLATION_MAX_RESPONSE_BYTES,
@@ -97,6 +98,7 @@ function militaryInstallationsProxy() {
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
return;
}
if (admitSameSite(req, res)) return;
const url = new URL(req.url, 'http://localhost');
const requested = validMilitaryInstallationBox(url.searchParams);
if (!requested) {
+41 -3
View File
@@ -54,14 +54,22 @@ async function call(handlers, url, init = {}) {
Object.assign(req, {
method: init.method || 'GET',
url: parsed.pathname.slice(mount.length) + parsed.search || '/',
headers: {},
headers: init.headers || {},
socket: { remoteAddress: '127.0.0.1' },
});
let status, headers, body;
let status, body;
let headers = {};
await handlers.get(mount)(req, {
// The cross-site gate answers through statusCode/setHeader, not writeHead.
set statusCode(s) {
status = s;
},
setHeader(name, value) {
headers[name] = value;
},
writeHead(s, h) {
status = s;
headers = h;
headers = { ...headers, ...h };
},
end(b) {
body = b;
@@ -166,6 +174,36 @@ test('zero egress: every Overpass consumer reaches real default handlers, and re
);
});
test('the installations proxy refuses cross-site and forwarded requests before any Overpass work', async (t) => {
env(t);
t.mock.method(globalThis, 'fetch', () =>
assert.fail('unexpected upstream fetch'),
);
const handlers = routes(militaryInstallationsProxy());
const url =
'/api/military-installations?south=48.8&west=2.2&north=48.9&east=2.4';
for (const headers of [
{ 'sec-fetch-site': 'cross-site' },
{ 'sec-fetch-site': 'same-site' },
{ origin: 'https://foreign.example' },
{ origin: 'null' },
{ 'sec-fetch-site': 'same-origin', 'x-forwarded-for': '203.0.113.9' },
]) {
const response = await call(handlers, url, { headers });
assert.equal(response.status, 403, JSON.stringify(headers));
assert.equal(response.headers.get('cache-control'), 'no-store');
}
for (const headers of [
{},
{ origin: 'http://localhost:4173', 'sec-fetch-site': 'same-origin' },
]) {
const response = await call(handlers, url, {
headers: { host: 'localhost:4173', ...headers },
});
assert.notEqual(response.status, 403, JSON.stringify(headers));
}
});
test('an unconfigured server is probed once and never sent a feature query', async (t) => {
env(t);
t.mock.method(globalThis, 'fetch', () =>