openvibers/openvibe.network Commits

Network: the internal API's loopback rule applies to every spelling of /internal/ (#112)

OP OpenVibers committed 10 Oct 2026 · 49fad0f3c6 · 2 files, +4 −3

nginx only. The internal API block (allow loopback, deny the rest, proxy to the app) was a case-sensitive `location /internal/`, so `/Internal/...` fell through to `location /`. The app already keeps its internal router to the lowercase path (test/security-session.test.js), so this is defence in depth: the block is now `location ~* ^/internal(/|$)`, and every spelling gets the loopback rule. Checked with `nginx -t` on the host.

Changes

ddiff --git a/deploy/nginx/openvibe.network.conf b/deploy/nginx/openvibe.network.conf
iindex 36828e3f..49182486 100644
---- a/deploy/nginx/openvibe.network.conf
++++ b/deploy/nginx/openvibe.network.conf
@@@ -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;
ddiff --git a/test/security-session.test.js b/test/security-session.test.js
iindex eb12777d..9e7f0fc9 100644
---- a/test/security-session.test.js
++++ b/test/security-session.test.js
@@@ -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);