security: fix the review leftovers (owner, 08 §6)
- Asterisk doesn't load app_system, func_shell, app_disa, res_agi, app_externalivr. - Turning two-step sign-in on asks for the password again. - *97/*98: one voicemail login at a time per phone, 10 an hour (sp-sub-vmlimit). - /prov: an unknown MAC gets the same 401 as a known one. - The login limiter sweeps keys past every window. Also the v1 checklist with the owner's results from 2026-10-21.
This commit is contained in:
1 parent
3c8828eedb
commit
8d5475cfbf
32 files changed
+501
-61
No files matched your search
@@ -285,6 +285,31 @@ async function main() {
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
// Voicemail logins: *97 above counted once; with 10 this hour, *98 doesn't reach a PIN prompt.
|
||||
const hour = new Date().toISOString().replace(/\D/g, '').slice(0, 10);
|
||||
const counted = await ami.command('database get SP/VMLOGIN 101');
|
||||
if (!counted.includes(`${hour}:1`)) problems.push(`*97 not counted: ${counted}`);
|
||||
await ami.command(`database put SP/VMLOGIN 101 ${hour}:10`);
|
||||
await ami.action({
|
||||
Action: 'Originate',
|
||||
Channel: 'Local/*98@sp-from-ext',
|
||||
Application: 'Wait',
|
||||
Data: '5',
|
||||
Variable: '__SP_EXT=101',
|
||||
Async: 'true',
|
||||
});
|
||||
let refused = false;
|
||||
for (let i = 0; i < 20 && !refused; i++) {
|
||||
await sleep(500);
|
||||
refused = /Playing 'an-error-has-occurred/.test(docker('logs', AST).out);
|
||||
}
|
||||
if (!refused) problems.push('*98 past the hourly voicemail login limit was not refused');
|
||||
else if (/VoiceMailMain\("[^"]*", "@default"\)/.test(docker('logs', AST).out))
|
||||
problems.push('*98 reached VoiceMailMain past the limit');
|
||||
else log('*98 past 10 logins an hour: refused');
|
||||
await ami.command('channel request hangup all');
|
||||
await waitIdle();
|
||||
|
||||
// eslint-disable-next-line no-control-regex -- strips Asterisk's ANSI colors
|
||||
const plainLog = docker('logs', AST).out.replace(/\x1b\[[0-9;]*m/g, '');
|
||||
problems.push(
|
||||
|
||||
@@ -105,10 +105,25 @@ describe.skipIf(!hasTestDb)('/api/devices and /prov', () => {
|
||||
expect((await prov('y000000000044.cfg')).body).toBe('#!version:1.0.0.1\n');
|
||||
});
|
||||
|
||||
it('unknown MAC and the other vendor’s file are 404; ten failures block the IP', async () => {
|
||||
expect((await prov('001565000000.cfg')).statusCode).toBe(404);
|
||||
expect((await prov(`cfg${device.mac}.xml`)).statusCode).toBe(404); // a Yealink here
|
||||
it('unknown MAC and the other vendor’s file answer like a known MAC; ten failures block the IP', async () => {
|
||||
// 08 §6: the answers mustn't tell which MACs exist.
|
||||
const known = await prov(`${device.mac}.cfg`);
|
||||
for (const f of ['001565000000.cfg', `cfg${device.mac}.xml`]) {
|
||||
const r = await prov(f);
|
||||
expect([r.statusCode, r.headers['www-authenticate'], r.body]).toEqual([
|
||||
401,
|
||||
known.headers['www-authenticate'],
|
||||
known.body,
|
||||
]);
|
||||
}
|
||||
expect((await prov('anything.boot')).statusCode).toBe(404);
|
||||
// with credentials they're failures, like a wrong password
|
||||
expect(
|
||||
(await prov('001565000000.cfg', '192.168.1.20', 'guess', '001565000000')).statusCode,
|
||||
).toBe(401);
|
||||
expect(
|
||||
(await prov(`cfg${device.mac}.xml`, '192.168.1.20', device.provisioningPassword)).statusCode,
|
||||
).toBe(401); // a Yealink here
|
||||
for (let i = 0; i < 8; i++)
|
||||
expect((await prov(`${device.mac}.cfg`, '192.168.1.20', 'guess')).statusCode).toBe(401);
|
||||
// 10 failures (2 unknown MACs, 8 wrong passwords): even the right password is refused now
|
||||
@@ -181,7 +196,7 @@ describe.skipIf(!hasTestDb)('/api/devices and /prov', () => {
|
||||
expect(
|
||||
(await prov('112233445566.cfg', '192.168.1.40', generic.provisioningPassword, '112233445566'))
|
||||
.statusCode,
|
||||
).toBe(404);
|
||||
).toBe(401);
|
||||
});
|
||||
|
||||
it('M10-T2: BLF keys named after their users, the time zone, and the phonebook', async () => {
|
||||
@@ -256,6 +271,6 @@ describe.skipIf(!hasTestDb)('/api/devices and /prov', () => {
|
||||
expect((await req('DELETE', `/api/devices/${device.id}`)).statusCode).toBe(200);
|
||||
expect(
|
||||
(await prov(`${device.mac}.cfg`, '192.168.1.50', device.provisioningPassword)).statusCode,
|
||||
).toBe(404);
|
||||
).toBe(401);
|
||||
});
|
||||
});
|
||||
@@ -44,12 +44,18 @@ describe.skipIf(!hasTestDb)('two-step sign-in', () => {
|
||||
secret = setup.secret;
|
||||
expect(setup.otpauthUrl).toMatch(/^otpauth:\/\/totp\/.+\?secret=[A-Z2-7]{32}&issuer=/);
|
||||
expect(setup.qr).toMatch(/^data:image\/png;base64,/);
|
||||
const bad = await req('POST', '/api/auth/totp/enable', { secret, code: '000000' });
|
||||
const password = ADMIN.password;
|
||||
const bad = await req('POST', '/api/auth/totp/enable', { secret, code: '000000', password });
|
||||
expect(bad.statusCode).toBe(400);
|
||||
const ok = await req('POST', '/api/auth/totp/enable', {
|
||||
secret,
|
||||
code: totpCode(secret, stepAt()),
|
||||
});
|
||||
// A session alone isn't enough: the password again, like turning it off.
|
||||
const code = totpCode(secret, stepAt());
|
||||
for (const payload of [
|
||||
{ secret, code },
|
||||
{ secret, code, password: 'not-it' },
|
||||
])
|
||||
expect((await req('POST', '/api/auth/totp/enable', payload)).statusCode).toBe(400);
|
||||
expect((await req('GET', '/api/auth/totp')).json().enabled).toBe(false);
|
||||
const ok = await req('POST', '/api/auth/totp/enable', { secret, code, password });
|
||||
expect(ok.statusCode, ok.body).toBe(200);
|
||||
recovery = ok.json().recoveryCodes;
|
||||
expect(recovery).toHaveLength(10);
|
||||
@@ -58,7 +64,7 @@ describe.skipIf(!hasTestDb)('two-step sign-in', () => {
|
||||
recoveryCodesLeft: 10,
|
||||
});
|
||||
expect(
|
||||
(await req('POST', '/api/auth/totp/enable', { secret, code: '123456' })).statusCode,
|
||||
(await req('POST', '/api/auth/totp/enable', { secret, code: '123456', password })).statusCode,
|
||||
).toBe(409);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user