mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-25 12:41:53 +08:00
393 lines
13 KiB
TypeScript
393 lines
13 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
applyAuthorizationTransformExecution,
|
|
authorizationRequestToTransformPacket,
|
|
compileAuthorizationBaselineRequest,
|
|
extractAuthorizationResourceValue,
|
|
parseAuthorizationRequestPacket,
|
|
replaceAuthorizationResourceValue,
|
|
} from './baseline-execution';
|
|
import { fingerprintAuthorizationComparisonValue } from './baseline-metadata';
|
|
|
|
function base64(value: string): string {
|
|
return btoa(value);
|
|
}
|
|
|
|
describe('authorization baseline execution primitives', () => {
|
|
it('parses a bounded request packet without discarding captured credentials', () => {
|
|
const packet = parseAuthorizationRequestPacket(base64([
|
|
'GET /api/orders/42 HTTP/1.1',
|
|
'Host: example.test',
|
|
'Cookie: session=secret',
|
|
'Authorization: Bearer secret',
|
|
'X-CSRF-Token: csrf-secret',
|
|
'Sec-Fetch-Site: same-origin',
|
|
'',
|
|
'',
|
|
].join('\r\n')));
|
|
expect(packet.method).toBe('GET');
|
|
expect(packet.headers).toEqual([
|
|
{ name: 'Host', value: 'example.test' },
|
|
{ name: 'Cookie', value: 'session=secret' },
|
|
{ name: 'Authorization', value: 'Bearer secret' },
|
|
{ name: 'X-CSRF-Token', value: 'csrf-secret' },
|
|
{ name: 'Sec-Fetch-Site', value: 'same-origin' },
|
|
]);
|
|
});
|
|
|
|
it('extracts and replaces a normalized path resource without changing the origin', () => {
|
|
const value = extractAuthorizationResourceValue(
|
|
'https://example.test/api/orders/42?view=full',
|
|
'',
|
|
'baseline-left',
|
|
{ location: 'path', path: 'path.segment[2]' },
|
|
'workspace-hmac-sha256:a'.padEnd(86, 'a'),
|
|
);
|
|
const replaced = replaceAuthorizationResourceValue(
|
|
'https://example.test/api/orders/42?view=full',
|
|
{ location: 'path', path: 'path.segment[2]' },
|
|
'84',
|
|
);
|
|
|
|
expect(atob(value.valueBase64)).toBe('42');
|
|
expect(replaced).toBe('https://example.test/api/orders/84?view=full');
|
|
});
|
|
|
|
it('addresses repeated query parameters by occurrence', () => {
|
|
const url = 'https://example.test/api/orders?id=42&view=full&id=84';
|
|
const value = extractAuthorizationResourceValue(
|
|
url,
|
|
'',
|
|
'baseline-right',
|
|
{ location: 'query', path: 'query.id[1]' },
|
|
'workspace-hmac-sha256:b'.padEnd(86, 'b'),
|
|
);
|
|
const replaced = replaceAuthorizationResourceValue(
|
|
url,
|
|
{ location: 'query', path: 'query.id[1]' },
|
|
'126',
|
|
);
|
|
|
|
expect(atob(value.valueBase64)).toBe('84');
|
|
expect(replaced).toBe('https://example.test/api/orders?id=42&view=full&id=126');
|
|
expect(() => extractAuthorizationResourceValue(
|
|
url,
|
|
'',
|
|
'baseline-right',
|
|
{ location: 'query', path: 'query.id' },
|
|
'workspace-hmac-sha256:b'.padEnd(86, 'b'),
|
|
)).toThrow('多个同名值');
|
|
});
|
|
|
|
it('compiles a read-only request while retaining the exact captured header block', async () => {
|
|
const comparisonKey = btoa(String.fromCharCode(...new Uint8Array(32).fill(7)))
|
|
.replace(/\+/g, '-')
|
|
.replace(/\//g, '_')
|
|
.replace(/=+$/, '');
|
|
const valueFingerprint = await fingerprintAuthorizationComparisonValue(comparisonKey, '84');
|
|
const raw = [
|
|
'GET /api/orders/42 HTTP/1.1',
|
|
'Host: example.test',
|
|
'Cookie: session=secret',
|
|
'Authorization: Bearer secret',
|
|
'',
|
|
'',
|
|
].join('\r\n');
|
|
const compiled = await compileAuthorizationBaselineRequest({
|
|
baselineId: 'baseline-left',
|
|
rawRequestBase64: base64(raw),
|
|
requestUrl: 'https://example.test/api/orders/42',
|
|
publicUrl: 'https://example.test/api/orders/:resource',
|
|
selector: { source: 'wire', location: 'path', path: 'path.segment[2]' },
|
|
replacement: {
|
|
version: 1,
|
|
baselineId: 'baseline-right',
|
|
source: 'wire',
|
|
location: 'path',
|
|
path: 'path.segment[2]',
|
|
valueType: 'string',
|
|
byteLength: 2,
|
|
valueBase64: base64('84'),
|
|
valueFingerprint,
|
|
},
|
|
comparisonKey,
|
|
isHttps: true,
|
|
});
|
|
|
|
const request = atob(compiled.rawRequestBase64);
|
|
expect(request).toContain('GET /api/orders/84 HTTP/1.1\r\n');
|
|
expect(request).toContain('Cookie: session=secret\r\n');
|
|
expect(request).toContain('Authorization: Bearer secret\r\n');
|
|
expect(compiled.resourceValueFingerprint).toBe(valueFingerprint);
|
|
});
|
|
|
|
it('replaces an explicit resource Header without copying another identity credential', async () => {
|
|
const comparisonKey = btoa(String.fromCharCode(...new Uint8Array(32).fill(11)))
|
|
.replace(/\+/g, '-')
|
|
.replace(/\//g, '_')
|
|
.replace(/=+$/, '');
|
|
const valueFingerprint = await fingerprintAuthorizationComparisonValue(comparisonKey, 'tenant-b');
|
|
const raw = [
|
|
'GET /api/orders HTTP/1.1',
|
|
'Host: example.test',
|
|
'Cookie: session=identity-a',
|
|
'X-Tenant-Id: tenant-a',
|
|
'',
|
|
'',
|
|
].join('\r\n');
|
|
const resource = extractAuthorizationResourceValue(
|
|
'https://example.test/api/orders',
|
|
base64(raw),
|
|
'baseline-left',
|
|
{ location: 'header', path: 'header.x-tenant-id' },
|
|
await fingerprintAuthorizationComparisonValue(comparisonKey, 'tenant-a'),
|
|
);
|
|
const compiled = await compileAuthorizationBaselineRequest({
|
|
baselineId: 'baseline-left',
|
|
rawRequestBase64: base64(raw),
|
|
requestUrl: 'https://example.test/api/orders',
|
|
publicUrl: 'https://example.test/api/orders',
|
|
selector: { source: 'wire', location: 'header', path: 'header.x-tenant-id' },
|
|
replacement: {
|
|
version: 1,
|
|
baselineId: 'baseline-right',
|
|
source: 'wire',
|
|
location: 'header',
|
|
path: 'header.x-tenant-id',
|
|
valueType: 'string',
|
|
byteLength: 8,
|
|
valueBase64: base64('tenant-b'),
|
|
valueFingerprint,
|
|
},
|
|
comparisonKey,
|
|
isHttps: true,
|
|
});
|
|
|
|
expect(atob(resource.valueBase64)).toBe('tenant-a');
|
|
expect(atob(compiled.rawRequestBase64)).toContain('X-Tenant-Id: tenant-b\r\n');
|
|
expect(atob(compiled.rawRequestBase64)).toContain('Cookie: session=identity-a\r\n');
|
|
expect(atob(compiled.rawRequestBase64)).not.toContain('session=identity-b');
|
|
});
|
|
|
|
it('replaces one GraphQL variable in a reviewed POST without changing the operation or credentials', async () => {
|
|
const comparisonKey = btoa(String.fromCharCode(...new Uint8Array(32).fill(13)))
|
|
.replace(/\+/g, '-')
|
|
.replace(/\//g, '_')
|
|
.replace(/=+$/, '');
|
|
const valueFingerprint = await fingerprintAuthorizationComparisonValue(
|
|
comparisonKey,
|
|
'84',
|
|
);
|
|
const body = JSON.stringify({
|
|
operationName: 'Order',
|
|
query: 'query Order($orderId: ID!) { order(id: $orderId) { id total } }',
|
|
variables: {
|
|
orderId: 42,
|
|
includeAudit: true,
|
|
},
|
|
});
|
|
const raw = [
|
|
'POST /graphql HTTP/1.1',
|
|
'Host: example.test',
|
|
'Content-Type: application/json',
|
|
`Content-Length: ${new TextEncoder().encode(body).byteLength}`,
|
|
'Cookie: session=identity-a',
|
|
'',
|
|
body,
|
|
].join('\r\n');
|
|
|
|
const compiled = await compileAuthorizationBaselineRequest({
|
|
baselineId: 'baseline-left',
|
|
rawRequestBase64: base64(raw),
|
|
requestUrl: 'https://example.test/graphql',
|
|
publicUrl: 'https://example.test/graphql',
|
|
selector: {
|
|
source: 'wire',
|
|
location: 'body',
|
|
path: 'body.variables.orderId',
|
|
},
|
|
replacement: {
|
|
version: 1,
|
|
baselineId: 'baseline-right',
|
|
source: 'wire',
|
|
location: 'body',
|
|
path: 'body.variables.orderId',
|
|
valueType: 'number',
|
|
byteLength: 2,
|
|
valueBase64: base64('84'),
|
|
valueFingerprint,
|
|
},
|
|
comparisonKey,
|
|
isHttps: true,
|
|
});
|
|
|
|
const compiledPacket = parseAuthorizationRequestPacket(compiled.rawRequestBase64);
|
|
const compiledBody = JSON.parse(new TextDecoder().decode(
|
|
compiledPacket.bytes.subarray(compiledPacket.bodyOffset),
|
|
));
|
|
expect(compiledBody.variables).toEqual({
|
|
orderId: 84,
|
|
includeAudit: true,
|
|
});
|
|
expect(compiledBody.query).toBe(
|
|
'query Order($orderId: ID!) { order(id: $orderId) { id total } }',
|
|
);
|
|
expect(atob(compiled.rawRequestBase64)).toContain('Cookie: session=identity-a\r\n');
|
|
expect(compiledPacket.headers.find(
|
|
(header) => header.name.toLowerCase() === 'content-length',
|
|
)?.value).toBe(String(new TextEncoder().encode(JSON.stringify(compiledBody)).byteLength));
|
|
});
|
|
|
|
it('addresses a GraphQL batch variable by its ordered operation index', async () => {
|
|
const comparisonKey = btoa(String.fromCharCode(...new Uint8Array(32).fill(17)))
|
|
.replace(/\+/g, '-')
|
|
.replace(/\//g, '_')
|
|
.replace(/=+$/, '');
|
|
const valueFingerprint = await fingerprintAuthorizationComparisonValue(
|
|
comparisonKey,
|
|
'user-b',
|
|
);
|
|
const body = JSON.stringify([
|
|
{
|
|
operationName: 'Viewer',
|
|
query: 'query Viewer { viewer { id } }',
|
|
variables: {},
|
|
},
|
|
{
|
|
operationName: 'User',
|
|
query: 'query User($userId: ID!) { user(id: $userId) { id } }',
|
|
variables: { userId: 'user-a' },
|
|
},
|
|
]);
|
|
const raw = [
|
|
'POST /graphql HTTP/1.1',
|
|
'Host: example.test',
|
|
'Content-Type: application/json',
|
|
`Content-Length: ${new TextEncoder().encode(body).byteLength}`,
|
|
'Cookie: session=identity-a',
|
|
'',
|
|
body,
|
|
].join('\r\n');
|
|
|
|
const compiled = await compileAuthorizationBaselineRequest({
|
|
baselineId: 'baseline-left',
|
|
rawRequestBase64: base64(raw),
|
|
requestUrl: 'https://example.test/graphql',
|
|
publicUrl: 'https://example.test/graphql',
|
|
selector: {
|
|
source: 'wire',
|
|
location: 'body',
|
|
path: 'body[1].variables.userId',
|
|
},
|
|
replacement: {
|
|
version: 1,
|
|
baselineId: 'baseline-right',
|
|
source: 'wire',
|
|
location: 'body',
|
|
path: 'body[1].variables.userId',
|
|
valueType: 'string',
|
|
byteLength: 6,
|
|
valueBase64: base64('user-b'),
|
|
valueFingerprint,
|
|
},
|
|
comparisonKey,
|
|
isHttps: true,
|
|
});
|
|
|
|
const compiledPacket = parseAuthorizationRequestPacket(compiled.rawRequestBase64);
|
|
const compiledBody = JSON.parse(new TextDecoder().decode(
|
|
compiledPacket.bytes.subarray(compiledPacket.bodyOffset),
|
|
));
|
|
expect(compiledBody.map((operation: { operationName: string }) => operation.operationName))
|
|
.toEqual(['Viewer', 'User']);
|
|
expect(compiledBody[1].variables.userId).toBe('user-b');
|
|
});
|
|
|
|
it('applies an identity-bound query signature without changing captured credentials', async () => {
|
|
const raw = base64([
|
|
'GET /api/orders/84?nonce=old&signature=old HTTP/1.1',
|
|
'Host: example.test',
|
|
'Cookie: session=identity-a',
|
|
'Authorization: Bearer identity-a',
|
|
'',
|
|
'',
|
|
].join('\r\n'));
|
|
const packet = authorizationRequestToTransformPacket(raw, 'https://example.test');
|
|
const compiled = await applyAuthorizationTransformExecution({
|
|
compiled: {
|
|
version: 1,
|
|
baselineId: 'baseline-left',
|
|
selector: { source: 'wire', location: 'path', path: 'path.segment[2]' },
|
|
method: 'GET',
|
|
url: 'https://example.test/api/orders/:resource',
|
|
isHttps: true,
|
|
rawRequestBase64: raw,
|
|
resourceValueFingerprint: 'workspace-hmac-sha256:a'.padEnd(88, 'a'),
|
|
packetFingerprint: `sha256:${'a'.repeat(64)}`,
|
|
},
|
|
execution: {
|
|
profileId: 'profile-left',
|
|
direction: 'request',
|
|
url: 'https://example.test/api/orders/84?nonce=fresh&signature=signed-84',
|
|
bodyBase64: packet.bodyBase64,
|
|
setHeaders: [],
|
|
removeHeaders: [],
|
|
logicalInput: {},
|
|
logicalOutput: {},
|
|
nodeDurations: [],
|
|
nodeTrace: [],
|
|
fieldChanges: [],
|
|
durationMs: 1,
|
|
},
|
|
origin: 'https://example.test',
|
|
allowedDestinations: ['query.nonce', 'query.signature'],
|
|
});
|
|
|
|
const request = atob(compiled.rawRequestBase64);
|
|
expect(request).toContain('GET /api/orders/84?nonce=fresh&signature=signed-84 HTTP/1.1');
|
|
expect(request).toContain('Cookie: session=identity-a');
|
|
expect(request).toContain('Authorization: Bearer identity-a');
|
|
});
|
|
|
|
it('rejects dynamic transforms that touch authentication headers', async () => {
|
|
const raw = base64([
|
|
'GET /api/orders/84?signature=old HTTP/1.1',
|
|
'Host: example.test',
|
|
'Cookie: session=identity-a',
|
|
'',
|
|
'',
|
|
].join('\r\n'));
|
|
const packet = authorizationRequestToTransformPacket(raw, 'https://example.test');
|
|
|
|
await expect(applyAuthorizationTransformExecution({
|
|
compiled: {
|
|
version: 1,
|
|
baselineId: 'baseline-left',
|
|
selector: { source: 'wire', location: 'path', path: 'path.segment[2]' },
|
|
method: 'GET',
|
|
url: 'https://example.test/api/orders/:resource',
|
|
isHttps: true,
|
|
rawRequestBase64: raw,
|
|
resourceValueFingerprint: 'workspace-hmac-sha256:a'.padEnd(88, 'a'),
|
|
packetFingerprint: `sha256:${'a'.repeat(64)}`,
|
|
},
|
|
execution: {
|
|
profileId: 'profile-left',
|
|
direction: 'request',
|
|
url: packet.url,
|
|
bodyBase64: packet.bodyBase64,
|
|
setHeaders: [{ name: 'Cookie', value: 'session=identity-b' }],
|
|
removeHeaders: [],
|
|
logicalInput: {},
|
|
logicalOutput: {},
|
|
nodeDurations: [],
|
|
nodeTrace: [],
|
|
fieldChanges: [],
|
|
durationMs: 1,
|
|
},
|
|
origin: 'https://example.test',
|
|
allowedDestinations: ['header.cookie'],
|
|
})).rejects.toThrow('认证材料');
|
|
});
|
|
});
|