security: review fixes (dialed-number filter, browser endpoint ACL, IVR direct dial, …)

The 08 §1 threat table checked line by line against the code, plus a review by a separate agent
that hadn't written it. Findings were reproduced with a failing test before each fix:

- Critical: "011…,4" turned the route's Goto into Goto(sp-route-…,011…,4,1), starting past the
  class check, so any extension could call abroad. FILTER on the dialed number (also stops "&"
  adding a Dial target). trunk-test.ts proves it.
- High: -web endpoints (desk phone's password, any transport) skipped the phone ACL; now
  127.0.0.1 only when it's on. call-test.ts: UDP refused, WebSocket from 127.0.0.1 registers.
- IVR direct dial no longer reaches paging groups or parked calls (sp-ivr-direct).
- Trunks identify_by = ip: a From of trunk-<id> from anywhere no longer rings phones.
- .dockerignore is an allowlist, so the public source archive can't pick up stray files.
- CSRF check on the matched route (/%61pi/… got around it); Linphone tokens masked in logs;
  /api/live checks Origin and re-checks the session; *45/** filter digits; pg_dump password in
  the environment; more webhook ranges blocked.

The review and what's left for the owner are in 08 §6.

Task: M11-T8
This commit is contained in:
iamdoubz committed 2026-10-20 12:19:47 -05:00
1 parent 0ff0f9292b
commit 5c46bd0e85
46 files changed
+555 -87

No files matched your search

+12
View File
@@ -73,6 +73,18 @@ describe.skipIf(!hasTestDb)('auth', () => {
expect(r.json().error.code).toBe('csrf');
});
it('M11-T8: a percent-encoded path gets the same CSRF check', async () => {
for (const url of ['/%61pi/auth/login', '/api%2Fauth/login', '//api/auth/login']) {
const r = await t.app.inject({
method: 'POST',
url,
payload: { email: ADMIN.email, password: adminPassword },
});
expect([403, 404], url).toContain(r.statusCode);
if (r.statusCode === 403) expect(r.json().error.code).toBe('csrf');
}
});
it('must_change_password blocks everything outside /api/auth/* until changed', async () => {
const cookie = cookieOf(await login(ADMIN.email, adminPassword));
const blocked = await get('/api/admin-probe', cookie);