mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
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:
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).
|
||||||
@@ -5086,6 +5086,8 @@ repointing `OVERPASS_UPSTREAMS` refetches instead of serving another instance's
|
|||||||
answers; untagged entries stop serving once an instance is configured.
|
answers; untagged entries stop serving once an instance is configured.
|
||||||
|
|
||||||
The shared transport guards `/api/overpass` and `/api/military-installations`.
|
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
|
With no configured upstream, each route serves existing cache data (including
|
||||||
expired entries, marked stale with the original cache/retrieval date) without
|
expired entries, marked stale with the original cache/retrieval date) without
|
||||||
refresh. An annotation miss returns HTTP 503, `OVERPASS_NOT_CONFIGURED`,
|
refresh. An annotation miss returns HTTP 503, `OVERPASS_NOT_CONFIGURED`,
|
||||||
|
|||||||
@@ -432,7 +432,9 @@
|
|||||||
"server/providers/overpass/transport.js",
|
"server/providers/overpass/transport.js",
|
||||||
"src/sources/httpBody.js",
|
"src/sources/httpBody.js",
|
||||||
"src/i18n/marker.js",
|
"src/i18n/marker.js",
|
||||||
"src/sources/rateLimit.js"
|
"src/sources/rateLimit.js",
|
||||||
|
"server/providers/common/same-site.js",
|
||||||
|
"src/localRequestGate.mjs"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { makeRateLimiter, clientKey } from './common/rate-limit.js';
|
import { makeRateLimiter, clientKey } from './common/rate-limit.js';
|
||||||
|
import { admitSameSite } from './common/same-site.js';
|
||||||
import {
|
import {
|
||||||
MILITARY_INSTALLATION_ELEMENT_CAP,
|
MILITARY_INSTALLATION_ELEMENT_CAP,
|
||||||
MILITARY_INSTALLATION_MAX_RESPONSE_BYTES,
|
MILITARY_INSTALLATION_MAX_RESPONSE_BYTES,
|
||||||
@@ -97,6 +98,7 @@ function militaryInstallationsProxy() {
|
|||||||
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
|
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (admitSameSite(req, res)) return;
|
||||||
const url = new URL(req.url, 'http://localhost');
|
const url = new URL(req.url, 'http://localhost');
|
||||||
const requested = validMilitaryInstallationBox(url.searchParams);
|
const requested = validMilitaryInstallationBox(url.searchParams);
|
||||||
if (!requested) {
|
if (!requested) {
|
||||||
|
|||||||
@@ -54,14 +54,22 @@ async function call(handlers, url, init = {}) {
|
|||||||
Object.assign(req, {
|
Object.assign(req, {
|
||||||
method: init.method || 'GET',
|
method: init.method || 'GET',
|
||||||
url: parsed.pathname.slice(mount.length) + parsed.search || '/',
|
url: parsed.pathname.slice(mount.length) + parsed.search || '/',
|
||||||
headers: {},
|
headers: init.headers || {},
|
||||||
socket: { remoteAddress: '127.0.0.1' },
|
socket: { remoteAddress: '127.0.0.1' },
|
||||||
});
|
});
|
||||||
let status, headers, body;
|
let status, body;
|
||||||
|
let headers = {};
|
||||||
await handlers.get(mount)(req, {
|
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) {
|
writeHead(s, h) {
|
||||||
status = s;
|
status = s;
|
||||||
headers = h;
|
headers = { ...headers, ...h };
|
||||||
},
|
},
|
||||||
end(b) {
|
end(b) {
|
||||||
body = 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) => {
|
test('an unconfigured server is probed once and never sent a feature query', async (t) => {
|
||||||
env(t);
|
env(t);
|
||||||
t.mock.method(globalThis, 'fetch', () =>
|
t.mock.method(globalThis, 'fetch', () =>
|
||||||
|
|||||||
Reference in new issue
Block a user