Files
cinny/src/app/utils/serverAcl.test.ts
T
jaredandClaude Opus 4.8 3e1106b2d9 fix(security): tab-nabbing hardening + /acl self-lockout guard (SEC-3/4)
SEC-3: add `noopener,noreferrer` to the 5 `window.open(_blank)` sites that
don't use the returned handle (UserChips, OidcManageAccount, OtherDevices x2,
Verification), closing reverse tab-nabbing. SSOStage is intentionally excluded —
it needs the window handle + intact opener for its origin-checked SSO
postMessage handshake.

SEC-4: guard the `/acl` slash command against bricking the room.
- Extract the ACL glob helpers (isValidServerPattern/globToRegExp/matchesAnyGlob)
  from RoomServerACL into a shared utils/serverAcl.ts (+ unit test) so the
  command and the settings editor validate identically.
- Default a MISSING allow list to `*` only when the room has NO existing ACL
  (a first `/acl -d x` otherwise sent `allow: []`, which bricks the room); an
  existing ACL's absent/empty allow is preserved, not silently widened.
- Reject invalid globs; fail CLOSED on the universally-catastrophic cases
  (empty allow, or a `*` deny) even when the local domain is unknown; and reject
  any change that would ban this homeserver (self-lockout).

Guard hardened per two review passes (fail-closed on unknown domain; no silent
federation widening).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 21:23:35 -04:00

45 lines
1.7 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { isValidServerPattern, globToRegExp, matchesAnyGlob } from './serverAcl';
test('isValidServerPattern: accepts hostnames, IPs, and globs', () => {
for (const v of [
'*',
'*.example.com',
'matrix.org',
'1.2.3.*',
'10.0.0.?',
'*.evil.*',
'*bad*',
]) {
assert.equal(isValidServerPattern(v), true, v);
}
});
test('isValidServerPattern: rejects empty, malformed, and non-glob junk', () => {
for (const v of ['', ' ', '.foo', 'foo.', 'a..b', '-', ':', 'under_score', 'a,b', 'a b']) {
assert.equal(isValidServerPattern(v), false, v);
}
});
test('globToRegExp: * = any run, ? = single char, other metachars escaped', () => {
assert.equal(globToRegExp('*').test('anything.org'), true);
assert.equal(globToRegExp('*.evil.com').test('a.evil.com'), true);
assert.equal(globToRegExp('*.evil.com').test('evil.com'), false); // needs the leading label
assert.equal(globToRegExp('1.2.3.?').test('1.2.3.4'), true);
assert.equal(globToRegExp('1.2.3.?').test('1.2.3.45'), false);
// The dot is a literal, not a regex wildcard.
assert.equal(globToRegExp('a.b').test('axb'), false);
});
test('globToRegExp: case-insensitive (hostnames + Synapse IGNORECASE)', () => {
assert.equal(globToRegExp('MATRIX.foo.org').test('matrix.foo.org'), true);
});
test('matchesAnyGlob: true if any glob matches (self-ban detection)', () => {
assert.equal(matchesAnyGlob('lotusguild.org', ['*']), true);
assert.equal(matchesAnyGlob('lotusguild.org', ['*.evil.com', 'lotus*.org']), true);
assert.equal(matchesAnyGlob('lotusguild.org', ['*.evil.com']), false);
assert.equal(matchesAnyGlob('lotusguild.org', []), false);
});