mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-11 11:10:13 +01:00
fix(cso): security expectations may list several safe statuses while excluding the vulnerable marker
(cherry picked from commit aa9f45e0ece0e12152e7d02bf88e1559ee42b1eb)
This commit is contained in:
1 parent
2a3b8ccedd
commit
0ce4e42856
4 files changed
+55
-11
No files matched your search
+30
-10
@@ -180,7 +180,7 @@ export interface HttpAssertion {
|
||||
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
expected: { status: number; includes?: string; excludes?: string };
|
||||
expected: { status: number | number[]; includes?: string; excludes?: string };
|
||||
vulnerable?: { status: number; includes?: string; excludes?: string };
|
||||
}
|
||||
export interface Command {
|
||||
@@ -648,16 +648,34 @@ export function validateVerificationObservation(value: unknown): VerificationObs
|
||||
inputHash: v.inputHash,
|
||||
};
|
||||
}
|
||||
function assertion(value: unknown, name: string): HttpAssertion {
|
||||
const MAX_SAFE_STATUSES = 8;
|
||||
function assertion(value: unknown, name: string, safeStatusSet = false): HttpAssertion {
|
||||
const v = object(value, name),
|
||||
expected = object(v.expected, `${name}.expected`);
|
||||
exact(v, ['name', 'path', 'method', 'headers', 'body', 'expected', 'vulnerable'], name);
|
||||
const oracle = (x: Record<string, any>, n: string) => {
|
||||
const httpStatus = (s: unknown) => Number.isInteger(s) && (s as number) >= 100 && (s as number) <= 599;
|
||||
const oracle = (x: Record<string, any>, n: string, statusSet = false) => {
|
||||
exact(x, ['status', 'includes', 'excludes'], n);
|
||||
if (!Number.isInteger(x.status) || x.status < 100 || x.status > 599)
|
||||
throw new CsoError('INVALID_SCHEMA', `${n}.status must be an HTTP status`);
|
||||
if (!Array.isArray(x.status)) {
|
||||
if (!httpStatus(x.status)) throw new CsoError('INVALID_SCHEMA', `${n}.status must be an HTTP status`);
|
||||
} else if (!statusSet) throw new CsoError('INVALID_SCHEMA', `${n}.status must be an HTTP status`);
|
||||
else if (
|
||||
!x.status.length ||
|
||||
x.status.length > MAX_SAFE_STATUSES ||
|
||||
!x.status.every(httpStatus) ||
|
||||
new Set(x.status).size !== x.status.length
|
||||
)
|
||||
throw new CsoError(
|
||||
'INVALID_SCHEMA',
|
||||
`${n}.status must be 1..${MAX_SAFE_STATUSES} distinct HTTP statuses`,
|
||||
);
|
||||
else if (typeof x.excludes !== 'string' || !x.excludes)
|
||||
throw new CsoError(
|
||||
'INVALID_SCHEMA',
|
||||
`${n} with a set of safe statuses must exclude the vulnerable marker`,
|
||||
);
|
||||
return {
|
||||
status: x.status,
|
||||
status: Array.isArray(x.status) ? [...x.status] : x.status,
|
||||
...(x.includes === undefined ? {} : { includes: string(x.includes, `${n}.includes`) }),
|
||||
...(x.excludes === undefined ? {} : { excludes: string(x.excludes, `${n}.excludes`) }),
|
||||
};
|
||||
@@ -683,7 +701,7 @@ function assertion(value: unknown, name: string): HttpAssertion {
|
||||
method: oneOf(v.method, ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], `${name}.method`),
|
||||
...(Object.keys(headers).length ? { headers } : {}),
|
||||
...(v.body === undefined ? {} : { body: string(v.body, `${name}.body`, 65536) }),
|
||||
expected: oracle(expected, `${name}.expected`),
|
||||
expected: oracle(expected, `${name}.expected`, safeStatusSet && v.vulnerable !== undefined),
|
||||
...(v.vulnerable === undefined ? {} : { vulnerable: oracle(object(v.vulnerable), `${name}.vulnerable`) }),
|
||||
};
|
||||
}
|
||||
@@ -692,13 +710,13 @@ export function securityAssertions(security: VerificationRequest['security']): H
|
||||
return Array.isArray(security) ? security : [security];
|
||||
}
|
||||
function securityAssertion(value: unknown): VerificationRequest['security'] {
|
||||
if (!Array.isArray(value)) return assertion(value, 'security');
|
||||
if (!Array.isArray(value)) return assertion(value, 'security', true);
|
||||
if (!value.length || value.length > MAX_SECURITY_ASSERTIONS)
|
||||
throw new CsoError(
|
||||
'INVALID_SCHEMA',
|
||||
`security must be one assertion or 1..${MAX_SECURITY_ASSERTIONS} assertions`,
|
||||
);
|
||||
const assertions = value.map((x, i) => assertion(x, `security[${i}]`));
|
||||
const assertions = value.map((x, i) => assertion(x, `security[${i}]`, true));
|
||||
if (new Set(assertions.map((x) => x.name)).size !== assertions.length)
|
||||
throw new CsoError('INVALID_SCHEMA', 'security assertion names must be distinct');
|
||||
return assertions;
|
||||
@@ -805,7 +823,9 @@ export function validateVerificationRequest(input: unknown): VerificationRequest
|
||||
'Verification needs a legitimate control, distinct before/fixed security oracles, existing tests, immutable test files, and boundary files',
|
||||
);
|
||||
const mutuallyExclusive = ({ expected: secure, vulnerable }: HttpAssertion) =>
|
||||
secure.status !== vulnerable!.status ||
|
||||
(Array.isArray(secure.status)
|
||||
? !secure.status.includes(vulnerable!.status)
|
||||
: secure.status !== vulnerable!.status) ||
|
||||
(secure.includes !== undefined &&
|
||||
vulnerable!.excludes !== undefined &&
|
||||
secure.includes.includes(vulnerable!.excludes)) ||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ interface Config {
|
||||
}
|
||||
function matches(status: number, body: string, oracle: HttpAssertion['expected']): boolean {
|
||||
return (
|
||||
status === oracle.status &&
|
||||
(Array.isArray(oracle.status) ? oracle.status.includes(status) : status === oracle.status) &&
|
||||
(oracle.includes === undefined || body.includes(oracle.includes)) &&
|
||||
(oracle.excludes === undefined || !body.includes(oracle.excludes))
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user