mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-26 21:21:53 +08:00
fix(authorization): keep capture across same-origin login
This commit is contained in:
@@ -3,7 +3,9 @@ import type {
|
|||||||
YakPocGenerateResult,
|
YakPocGenerateResult,
|
||||||
} from '@/types/models';
|
} from '@/types/models';
|
||||||
import type { CapabilityDomainHandler } from '../capability-context';
|
import type { CapabilityDomainHandler } from '../capability-context';
|
||||||
import { allowedTarget, requireScope } from '../capability-context';
|
import {
|
||||||
|
allowedTarget, PAIRED_BROWSER_INSTANCE_ACCESS_ID, requireScope,
|
||||||
|
} from '../capability-context';
|
||||||
import {
|
import {
|
||||||
clearNetworkRequests,
|
clearNetworkRequests,
|
||||||
exportNetworkRequest,
|
exportNetworkRequest,
|
||||||
@@ -30,7 +32,12 @@ export const networkCapabilityHandler: CapabilityDomainHandler = {
|
|||||||
captureBody: input.captureBody === true,
|
captureBody: input.captureBody === true,
|
||||||
maxEntries: typeof input.maxEntries === 'number' ? input.maxEntries : undefined,
|
maxEntries: typeof input.maxEntries === 'number' ? input.maxEntries : undefined,
|
||||||
maxBodyBytes: typeof input.maxBodyBytes === 'number' ? input.maxBodyBytes : undefined,
|
maxBodyBytes: typeof input.maxBodyBytes === 'number' ? input.maxBodyBytes : undefined,
|
||||||
}, { kind: 'grant', grantId: grant.id, expiresAt: grant.expiresAt });
|
}, {
|
||||||
|
kind: 'grant',
|
||||||
|
grantId: grant.id,
|
||||||
|
expiresAt: grant.expiresAt,
|
||||||
|
followSameOriginNavigation: grant.id === PAIRED_BROWSER_INSTANCE_ACCESS_ID,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (method === 'browser.network.status') return networkCaptureStatus(target);
|
if (method === 'browser.network.status') return networkCaptureStatus(target);
|
||||||
if (method === 'browser.network.list') {
|
if (method === 'browser.network.list') {
|
||||||
|
|||||||
@@ -185,6 +185,37 @@ describe('network capture lifecycle, budget and persistence', () => {
|
|||||||
expect((await networkCaptureStatus({ tabId: 43, frameId: 0, documentId: 'document-cross-origin' })).active).toBe(false);
|
expect((await networkCaptureStatus({ tabId: 43, frameId: 0, documentId: 'document-cross-origin' })).active).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('continues a paired-browser capture across a same-origin login navigation', async () => {
|
||||||
|
setTarget(45, 'http://localhost:8080/logic/user/login', 'document-login');
|
||||||
|
const before = await start(
|
||||||
|
45,
|
||||||
|
{ captureHeaders: true, captureBody: true },
|
||||||
|
{
|
||||||
|
kind: 'grant',
|
||||||
|
grantId: 'paired-browser-instance',
|
||||||
|
expiresAt: Number.MAX_SAFE_INTEGER,
|
||||||
|
followSameOriginNavigation: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await committed(45, 'http://localhost:8080/logic/user/profile', 'document-profile');
|
||||||
|
|
||||||
|
const after = await networkCaptureStatus({ tabId: 45, frameId: 0, documentId: 'document-profile' });
|
||||||
|
expect(after).toMatchObject({ active: true, startedAt: before.startedAt });
|
||||||
|
|
||||||
|
await committed(45, 'http://other.example/landing', 'document-other');
|
||||||
|
expect((await networkCaptureStatus({ tabId: 45, frameId: 0, documentId: 'document-other' })).active).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not silently extend a regular scoped grant across navigation', async () => {
|
||||||
|
setTarget(46, 'https://scoped.example.test/start', 'document-start');
|
||||||
|
await start(46, {}, { kind: 'grant', grantId: 'scoped-grant', expiresAt: NOW + 60_000 });
|
||||||
|
|
||||||
|
await committed(46, 'https://scoped.example.test/next', 'document-next');
|
||||||
|
|
||||||
|
expect((await networkCaptureStatus({ tabId: 46, frameId: 0, documentId: 'document-next' })).active).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('does not retain a cross-origin navigation request before the commit boundary is processed', async () => {
|
it('does not retain a cross-origin navigation request before the commit boundary is processed', async () => {
|
||||||
setTarget(44, 'https://source.example.test/start', 'document-source');
|
setTarget(44, 'https://source.example.test/start', 'document-source');
|
||||||
await start(44);
|
await start(44);
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ const CAPTURED_RESOURCE_TYPES = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
type CapturePersistence = 'pending' | 'persisted' | 'memory-only' | 'degraded';
|
type CapturePersistence = 'pending' | 'persisted' | 'memory-only' | 'degraded';
|
||||||
type CaptureOwner = { kind: 'local' } | { kind: 'grant'; grantId: string; expiresAt: number };
|
type CaptureOwner = { kind: 'local' } | {
|
||||||
|
kind: 'grant'; grantId: string; expiresAt: number; followSameOriginNavigation?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
interface CaptureSession {
|
interface CaptureSession {
|
||||||
target: BrowserTarget;
|
target: BrowserTarget;
|
||||||
@@ -270,7 +272,7 @@ function addRestoredSession(value: PersistedCaptureSession): boolean {
|
|||||||
options: normalizedOptions(value.options),
|
options: normalizedOptions(value.options),
|
||||||
records,
|
records,
|
||||||
owner: value.owner?.kind === 'grant' && typeof value.owner.grantId === 'string' && typeof value.owner.expiresAt === 'number'
|
owner: value.owner?.kind === 'grant' && typeof value.owner.grantId === 'string' && typeof value.owner.expiresAt === 'number'
|
||||||
? value.owner
|
? { ...value.owner, followSameOriginNavigation: value.owner.followSameOriginNavigation === true }
|
||||||
: { kind: 'local' },
|
: { kind: 'local' },
|
||||||
retainedBytes,
|
retainedBytes,
|
||||||
recordBytes,
|
recordBytes,
|
||||||
@@ -283,7 +285,11 @@ function addRestoredSession(value: PersistedCaptureSession): boolean {
|
|||||||
totalRecordCount += records.length;
|
totalRecordCount += records.length;
|
||||||
totalRetainedBytes += retainedBytes;
|
totalRetainedBytes += retainedBytes;
|
||||||
const ownerWasValid = value.owner?.kind === 'local'
|
const ownerWasValid = value.owner?.kind === 'local'
|
||||||
|| (value.owner?.kind === 'grant' && typeof value.owner.grantId === 'string' && typeof value.owner.expiresAt === 'number');
|
|| (value.owner?.kind === 'grant'
|
||||||
|
&& typeof value.owner.grantId === 'string'
|
||||||
|
&& typeof value.owner.expiresAt === 'number'
|
||||||
|
&& (value.owner.followSameOriginNavigation === undefined
|
||||||
|
|| typeof value.owner.followSameOriginNavigation === 'boolean'));
|
||||||
return records.length === value.records.length && ownerWasValid && !existing;
|
return records.length === value.records.length && ownerWasValid && !existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,7 +611,7 @@ browser.webRequest.onErrorOccurred.addListener((details) => {
|
|||||||
dispatchCaptureEvent(errorRecord, details);
|
dispatchCaptureEvent(errorRecord, details);
|
||||||
}, { urls: ['<all_urls>'], types: [...CAPTURED_RESOURCE_TYPES] });
|
}, { urls: ['<all_urls>'], types: [...CAPTURED_RESOURCE_TYPES] });
|
||||||
|
|
||||||
async function rebindLocalCaptureAfterNavigation(details: {
|
async function rebindCaptureAfterNavigation(details: {
|
||||||
tabId: number;
|
tabId: number;
|
||||||
frameId: number;
|
frameId: number;
|
||||||
documentId?: string;
|
documentId?: string;
|
||||||
@@ -613,7 +619,8 @@ async function rebindLocalCaptureAfterNavigation(details: {
|
|||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await restorePromise;
|
await restorePromise;
|
||||||
const session = captureSessions.get(details.tabId);
|
const session = captureSessions.get(details.tabId);
|
||||||
if (!session || session.owner.kind !== 'local' || session.target.frameId !== details.frameId) return;
|
if (!session || session.target.frameId !== details.frameId
|
||||||
|
|| (session.owner.kind === 'grant' && !session.owner.followSameOriginNavigation)) return;
|
||||||
if (details.documentId && session.target.documentId === details.documentId) return;
|
if (details.documentId && session.target.documentId === details.documentId) return;
|
||||||
let isolationBoundary: string | undefined;
|
let isolationBoundary: string | undefined;
|
||||||
try {
|
try {
|
||||||
@@ -639,7 +646,7 @@ async function rebindLocalCaptureAfterNavigation(details: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
browser.webNavigation.onCommitted.addListener((details) => {
|
browser.webNavigation.onCommitted.addListener((details) => {
|
||||||
void rebindLocalCaptureAfterNavigation(details).catch(() => undefined);
|
void rebindCaptureAfterNavigation(details).catch(() => undefined);
|
||||||
});
|
});
|
||||||
browser.tabs.onRemoved.addListener((tabId) => {
|
browser.tabs.onRemoved.addListener((tabId) => {
|
||||||
if (!deleteSession(tabId)) return;
|
if (!deleteSession(tabId)) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user