Restrict delivery HTTP to loopback and fix grant races.

Require trusted loopback requests for proactive send, accept hyphenated WhatsApp group JIDs, and serialize access-grant mutations to avoid lost concurrent updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-06 14:45:37 +08:00
parent 01924e5f7e
commit cbbf373b6a
8 changed files with 411 additions and 240 deletions

View file

@ -34,6 +34,17 @@ function grant(overrides = {}) {
});
}
test('ACCESS_GRANT_GROUP_JID_PATTERN accepts legacy hyphen group JIDs', () => {
const legacy = '123456789-120363000000000000@g.us';
assert.doesNotThrow(() => validateAccessGrant({
...emptyAccessGrant(),
globalAdmins: ['8618111111111'],
groups: {
[legacy]: { admins: [], members: [] },
},
}));
});
test('normalizeAccessPhone accepts E.164, JIDs, and rejects LID servers', () => {
assert.equal(normalizeAccessPhone('+86 181-4238-7786'), '8618142387786');
assert.equal(normalizeAccessPhone('8618142387786@s.whatsapp.net'), '8618142387786');

View file

@ -153,6 +153,45 @@ test('delivery HTTP maps only stable delivery errors to HTTP status codes', asyn
});
});
test('delivery HTTP rejects non-loopback Host', async () => {
const { service, calls } = serviceFixture();
const handler = createDeliveryHttpHandler(service);
const chunks = [];
const response = {
destroyed: false,
writableEnded: false,
writeHead(status) {
this.status = status;
},
end(body) {
this.writableEnded = true;
chunks.push(body);
},
once() {},
off() {},
};
await handler({
method: 'POST',
headers: {
host: '192.168.1.10:3080',
'content-type': 'application/json',
},
socket: { remoteAddress: '192.168.1.20' },
once() {},
off() {},
async *[Symbol.asyncIterator]() {
yield Buffer.from(JSON.stringify({
botId: 'bot_one', targetId: 'daily-report', text: 'nope',
}));
},
}, response);
assert.equal(response.status, 403);
assert.deepEqual(JSON.parse(chunks.join('')), {
error: { code: 'forbidden', message: 'forbidden', details: {} },
});
assert.deepEqual(calls, []);
});
test('delivery HTTP installs one exact WebServer route with Cordis lifecycle ownership', () => {
const { service } = serviceFixture();
const registrations = [];