openvibers/openvibe.network Commits
Network: the internal API's loopback rule applies to every spelling of /internal/ (#112)
Changes
@@@ -124,7 +124,8 @@ server {}# ── Internal API (server-to-server only, never public) ──-- location /internal/ {++ # Case-insensitive: Express routes /Internal/... too, so every spelling takes the loopback rule.++ location ~* ^/internal(/|$) {allow 127.0.0.1;allow ::1;deny all;@@@ -68,8 +68,8 @@ const sign = (sub, opts = {}) => jwt.sign({ sub, id: sub, username: 'u' }, keys.assert.strictEqual(r.status, 401); assert.strictEqual(r.body.code, 'token.missing');r = await fetch(`${base}/internal/verify-token`, { headers: { 'x-internal-key': 'k'.repeat(32) } }).then((x) => x.status);assert.strictEqual(r, 401, 'and the retired key does not resurrect it');-- // nginx only lets loopback reach `location /internal/` (case-sensitive); Express mounts are not,-- // so another spelling must not reach the internal router from outside.++ // nginx lets only loopback reach `location ~* ^/internal(/|$)` (every spelling); the app also keeps++ // the internal router to the lowercase path, so another spelling never reaches it either way.const get = (p, headers) => fetch(base + p, { headers }).then((x) => x.status);assert.strictEqual(await get('/INTERNAL/coins/stats', {}), 404, '/INTERNAL/... is not the internal API');assert.strictEqual(await get('/Internal/coins/stats', {}), 404);
@@ -124,7 +124,8 @@ server {
@@ -124,7 +124,8 @@ server {
}
}
# ── Internal API (server-to-server only, never public) ──
# ── Internal API (server-to-server only, never public) ──
- location /internal/ {
+ # Case-insensitive: Express routes /Internal/... too, so every spelling takes the loopback rule.
+ location ~* ^/internal(/|$) {
allow 127.0.0.1;
allow 127.0.0.1;
allow ::1;
allow ::1;
deny all;
deny all;
@@ -68,8 +68,8 @@ const sign = (sub, opts = {}) => jwt.sign({ sub, id: sub, username: 'u' }, keys.
@@ -68,8 +68,8 @@ const sign = (sub, opts = {}) => jwt.sign({ sub, id: sub, username: 'u' }, keys.
assert.strictEqual(r.status, 401); assert.strictEqual(r.body.code, 'token.missing');
assert.strictEqual(r.status, 401); assert.strictEqual(r.body.code, 'token.missing');
r = await fetch(`${base}/internal/verify-token`, { headers: { 'x-internal-key': 'k'.repeat(32) } }).then((x) => x.status);
r = await fetch(`${base}/internal/verify-token`, { headers: { 'x-internal-key': 'k'.repeat(32) } }).then((x) => x.status);
assert.strictEqual(r, 401, 'and the retired key does not resurrect it');
assert.strictEqual(r, 401, 'and the retired key does not resurrect it');
- // nginx only lets loopback reach `location /internal/` (case-sensitive); Express mounts are not,
- // so another spelling must not reach the internal router from outside.
+ // nginx lets only loopback reach `location ~* ^/internal(/|$)` (every spelling); the app also keeps
+ // the internal router to the lowercase path, so another spelling never reaches it either way.
const get = (p, headers) => fetch(base + p, { headers }).then((x) => x.status);
const get = (p, headers) => fetch(base + p, { headers }).then((x) => x.status);
assert.strictEqual(await get('/INTERNAL/coins/stats', {}), 404, '/INTERNAL/... is not the internal API');
assert.strictEqual(await get('/INTERNAL/coins/stats', {}), 404, '/INTERNAL/... is not the internal API');
assert.strictEqual(await get('/Internal/coins/stats', {}), 404);
assert.strictEqual(await get('/Internal/coins/stats', {}), 404);