test(stalker): reject noncanonical do auth replay

This commit is contained in:
4gray committed 2026-07-27 12:18:46 +02:00
1 parent 317f4ffdad
commit 6ebf5fd55a
3 files changed
+392 -1

No files matched your search

@@ -0,0 +1,323 @@
{
"description": "Synthetic string do auth near-miss is incompatible and forbids a second profile.",
"entry": {
"origin": "portal",
"path": "/c/"
},
"expectedEndpoint": {
"origin": "portal",
"path": "/portal.php"
},
"failOnUnexpectedRequest": true,
"initialState": "start",
"origins": {
"portal": {}
},
"phases": [
{
"expectations": [
{
"cardinality": {
"max": 1,
"min": 1
},
"id": "anonymous-probe",
"method": "GET",
"operation": "anonymous-probe",
"origin": "portal",
"path": "/c/",
"request": {
"body": {
"kind": "absent"
},
"cookies": {
"absent": [
"mac",
"session"
],
"attributes": {},
"exact": {},
"present": []
},
"headers": {
"absent": [
"authorization",
"cookie"
],
"exact": {},
"present": []
},
"query": {
"absent": [
"device_id",
"device_id2",
"mac",
"password",
"signature",
"signature2",
"sn",
"token",
"username"
],
"exact": {},
"present": []
}
},
"response": {
"body": {
"kind": "empty"
},
"headers": {},
"status": 204
}
},
{
"cardinality": {
"max": 1,
"min": 1
},
"id": "handshake",
"method": "GET",
"operation": "handshake",
"origin": "portal",
"path": "/portal.php",
"request": {
"body": {
"kind": "absent"
},
"cookies": {
"absent": [
"session"
],
"attributes": {},
"exact": {
"mac": {
"kind": "ref",
"symbol": "mac"
}
},
"present": []
},
"headers": {
"absent": [
"authorization"
],
"exact": {},
"present": []
},
"query": {
"absent": [
"password",
"token",
"username"
],
"exact": {
"action": "handshake",
"type": "stb"
},
"present": []
}
},
"response": {
"body": {
"kind": "json",
"value": {
"js": {
"token": {
"kind": "generate",
"symbol": "session-token",
"valueKind": "token"
}
}
}
},
"headers": {
"content-type": [
"application/json"
]
},
"status": 200
}
},
{
"cardinality": {
"max": 1,
"min": 1
},
"id": "profile-first",
"method": "GET",
"operation": "profile-first",
"origin": "portal",
"path": "/portal.php",
"request": {
"body": {
"kind": "absent"
},
"cookies": {
"absent": [
"session"
],
"attributes": {},
"exact": {
"mac": {
"kind": "ref",
"symbol": "mac"
}
},
"present": []
},
"headers": {
"absent": [],
"exact": {
"authorization": {
"kind": "parts",
"parts": [
{
"kind": "literal",
"value": "Bearer "
},
{
"kind": "ref",
"symbol": "session-token"
}
]
}
},
"present": []
},
"query": {
"absent": [
"password",
"username"
],
"exact": {
"action": "get_profile",
"auth_second_step": "0",
"type": "stb"
},
"present": []
}
},
"response": {
"body": {
"kind": "json",
"value": {
"js": {
"status": 2
}
}
},
"headers": {
"content-type": [
"application/json"
]
},
"status": 200
}
},
{
"cardinality": {
"max": 1,
"min": 1
},
"id": "do-auth-noncanonical-string",
"method": "GET",
"operation": "do-auth-noncanonical-string",
"origin": "portal",
"path": "/portal.php",
"request": {
"body": {
"kind": "absent"
},
"cookies": {
"absent": [
"session"
],
"attributes": {},
"exact": {
"mac": {
"kind": "ref",
"symbol": "mac"
}
},
"present": []
},
"headers": {
"absent": [],
"exact": {
"authorization": {
"kind": "parts",
"parts": [
{
"kind": "literal",
"value": "Bearer "
},
{
"kind": "ref",
"symbol": "session-token"
}
]
}
},
"present": []
},
"query": {
"absent": [],
"exact": {
"action": "do_auth",
"login": {
"kind": "ref",
"symbol": "username"
},
"password": {
"kind": "ref",
"symbol": "password"
},
"type": "stb"
},
"present": []
}
},
"response": {
"body": {
"kind": "json",
"value": {
"js": "true"
}
},
"headers": {
"content-type": [
"application/json"
]
},
"status": 200
}
}
],
"mode": "ordered",
"name": "noncanonical-do-auth-string",
"nextState": "complete",
"state": "start"
}
],
"scenarioId": "authentication-noncanonical-do-auth-string",
"schemaVersion": 1,
"symbols": [
{
"kind": "generate",
"symbol": "mac",
"valueKind": "mac"
},
{
"kind": "generate",
"symbol": "username",
"valueKind": "credential"
},
{
"kind": "generate",
"symbol": "password",
"valueKind": "credential"
}
],
"terminalState": "complete"
}
@@ -1,5 +1,5 @@
{
"description": "Synthetic noncanonical do auth success still permits the second profile.",
"description": "Synthetic numeric do auth near-miss is incompatible and forbids a second profile.",
"entry": {
"origin": "portal",
"path": "/c/"
@@ -1,6 +1,7 @@
/* eslint-disable max-lines -- The committed corpus matrix and its typed deterministic driver form one auditable contract. */
import { readdirSync, readFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { classifyStalkerDoAuth } from '@iptvnator/portal/stalker/protocol';
import { createReplayRun, type ReplayRun } from './replay-run.js';
import { parseReplayFixtureText } from './replay-schema.js';
import type {
@@ -26,6 +27,7 @@ const FIXTURE_ROOT = resolve(
const EXPECTED_SCENARIOS = [
'authentication-blocked-profile',
'authentication-noncanonical-do-auth',
'authentication-noncanonical-do-auth-string',
'authentication-saved-rejected-fresh',
'authentication-status2-second-step',
'authentication-three-attempt-limit',
@@ -641,6 +643,72 @@ describe('committed replay fixture corpus', () => {
}
}
});
it('terminates both noncanonical do_auth near misses before a second profile', () => {
const cases = [
{
expectedJs: 1,
fileName: 'noncanonical-do-auth.json',
scenarioId: 'authentication-noncanonical-do-auth',
},
{
expectedJs: 'true',
fileName: 'noncanonical-do-auth-string.json',
scenarioId: 'authentication-noncanonical-do-auth-string',
},
] as const;
for (const current of cases) {
const fixture = parseReplayFixtureText(
readFileSync(
join(FIXTURE_ROOT, 'authentication', current.fileName),
'utf8'
)
);
const expectations = fixture.phases.flatMap(
(phase) => phase.expectations
);
const profileSteps = expectations
.filter(
(expectation) =>
expectation.request.query.exact['action'] ===
'get_profile'
)
.map(
(expectation) =>
expectation.request.query.exact['auth_second_step']
);
const doAuth = expectations.find(
(expectation) =>
expectation.request.query.exact['action'] === 'do_auth'
);
if (doAuth?.response.body.kind !== 'json') {
throw new Error(
`Noncanonical fixture lacks do_auth JSON: ${fixture.scenarioId}.`
);
}
const doAuthValue = doAuth.response.body.value as {
readonly js?: unknown;
};
const terminalPhase = fixture.phases.find((phase) =>
phase.expectations.includes(doAuth)
);
expect(fixture).toMatchObject({
failOnUnexpectedRequest: true,
scenarioId: current.scenarioId,
});
expect(doAuthValue.js).toBe(current.expectedJs);
expect(
classifyStalkerDoAuth({ js: doAuthValue.js })
).toEqual({
kind: 'failure',
reason: 'incompatible-response',
});
expect(profileSteps).toEqual(['0']);
expect(terminalPhase?.nextState).toBe(fixture.terminalState);
}
});
});
function compareCodeUnits(left: string, right: string): number {