Skill Tracer
Skills › gstack › browse › cdp-allowlist.ts

cdp-allowlist.ts

235 lines · browse/src/cdp-allowlist.ts at 3f79d81

1/**
2 * CDP method allow-list (T2: deny-default).
3 *
4 * Codex outside-voice T2: allow-default with a deny-list is backwards because
5 * Target.*, Browser.*, Runtime.evaluate, Page.addScriptToEvaluateOnNewDocument,
6 * Fetch.*, IO.read, etc. are all dangerous and easy to forget. Default-deny
7 * inverts the failure mode: missing a method means it's blocked (annoying),
8 * not exposed (silent compromise).
9 *
10 * Each entry has:
11 * - domain.method unique CDP identifier
12 * - scope "tab" | "browser" — controls T7 mutex tier
13 * - output "trusted" | "untrusted" — wraps result if "untrusted"
14 * - justification why this method is safe to allow
15 *
16 * Add entries via PR. CI lint (cdp-allowlist.test.ts) ensures every entry has all 4 fields.
17 */
19export type CdpScope = 'tab' | 'browser';
20export type CdpOutput = 'trusted' | 'untrusted';
22export interface CdpAllowEntry {
23 domain: string;
24 method: string;
25 scope: CdpScope;
26 output: CdpOutput;
27 justification: string;
30export const CDP_ALLOWLIST: ReadonlyArray<CdpAllowEntry> = Object.freeze([
31 // ─── Accessibility (read-only) ─────────────────────────────
32 {
33 domain: 'Accessibility',
34 method: 'getFullAXTree',
35 scope: 'tab',
36 output: 'untrusted',
37 justification: 'Read-only AX tree extraction. Output is third-party page content; wrap in UNTRUSTED.',
38 },
39 {
40 domain: 'Accessibility',
41 method: 'getPartialAXTree',
42 scope: 'tab',
43 output: 'untrusted',
44 justification: 'Read-only AX tree subtree by node. Output is third-party page content.',
45 },
46 {
47 domain: 'Accessibility',
48 method: 'getRootAXNode',
49 scope: 'tab',
50 output: 'untrusted',
51 justification: 'Read-only root AX node accessor.',
52 },
53 // ─── DOM (read-only inspection) ────────────────────────────
54 {
55 domain: 'DOM',
56 method: 'describeNode',
57 scope: 'tab',
58 output: 'untrusted',
59 justification: 'Inspect a DOM node by backend ID; pure read.',
60 },
61 {
62 domain: 'DOM',
63 method: 'getBoxModel',
64 scope: 'tab',
65 output: 'trusted',
66 justification: 'Pure geometric data (box dimensions). No page content leaks; safe trusted.',
67 },
68 {
69 domain: 'DOM',
70 method: 'getNodeForLocation',
71 scope: 'tab',
72 output: 'trusted',
73 justification: 'Pure coordinate→nodeId mapping; no content leak.',
74 },
75 // ─── CSS (read-only) ───────────────────────────────────────
76 {
77 domain: 'CSS',
78 method: 'getMatchedStylesForNode',
79 scope: 'tab',
80 output: 'untrusted',
81 justification: 'Read computed cascade for a node; output may contain attacker-controlled selectors.',
82 },
83 {
84 domain: 'CSS',
85 method: 'getComputedStyleForNode',
86 scope: 'tab',
87 output: 'trusted',
88 justification: 'Computed style values are bounded (CSS keywords/numbers); safe trusted.',
89 },
90 {
91 domain: 'CSS',
92 method: 'getInlineStylesForNode',
93 scope: 'tab',
94 output: 'untrusted',
95 justification: 'Inline style content may contain attacker-controlled custom-property values.',
96 },
97 // ─── Performance metrics ───────────────────────────────────
98 {
99 domain: 'Performance',
100 method: 'getMetrics',
101 scope: 'tab',
102 output: 'trusted',
103 justification: 'Pure numeric metrics (timing, layout count); safe.',
104 },
105 {
106 domain: 'Performance',
107 method: 'enable',
108 scope: 'tab',
109 output: 'trusted',
110 justification: 'Domain enable; no content; required prerequisite for getMetrics.',
111 },
112 {
113 domain: 'Performance',
114 method: 'disable',
115 scope: 'tab',
116 output: 'trusted',
117 justification: 'Domain disable; no content.',
118 },
119 // ─── Tracing (event capture) ───────────────────────────────
120 // NOTE: Tracing.start can capture cross-tab data depending on categories.
121 // We mark it browser-scoped to acquire the global lock when in use.
122 {
123 domain: 'Tracing',
124 method: 'start',
125 scope: 'browser',
126 output: 'trusted',
127 justification: 'Trace category capture. Browser-scoped to serialize against other CDP ops.',
128 },
129 {
130 domain: 'Tracing',
131 method: 'end',
132 scope: 'browser',
133 output: 'untrusted',
134 justification: 'Trace dump may contain URLs and page data; wrap.',
135 },
136 // ─── Emulation (viewport/device) ───────────────────────────
137 {
138 domain: 'Emulation',
139 method: 'setDeviceMetricsOverride',
140 scope: 'tab',
141 output: 'trusted',
142 justification: 'Viewport/scale override on the active tab.',
143 },
144 {
145 domain: 'Emulation',
146 method: 'clearDeviceMetricsOverride',
147 scope: 'tab',
148 output: 'trusted',
149 justification: 'Clear viewport override.',
150 },
151 {
152 domain: 'Emulation',
153 method: 'setUserAgentOverride',
154 scope: 'tab',
155 output: 'trusted',
156 justification: 'UA override on the active tab. NOTE: changes affect future requests; fine for tests.',
157 },
158 {
159 domain: 'Emulation',
160 method: 'setEmulatedMedia',
161 scope: 'tab',
162 output: 'trusted',
163 justification: 'Media type/feature override (prefers-color-scheme, prefers-reduced-motion, prefers-contrast, forced-colors) so a11y and dark-mode CSS branches are testable. Returns an empty result; no page content. NOTE: like setUserAgentOverride the override persists on the tab until cleared with an empty features array.',
164 },
165 {
166 domain: 'Emulation',
167 method: 'setCPUThrottlingRate',
168 scope: 'tab',
169 output: 'trusted',
170 justification: 'CPU slowdown multiplier on the active tab, for measuring performance on a realistic low-end client instead of the developer workstation. Same domain and mutating character as setDeviceMetricsOverride; affects only timing, reads nothing, exfiltrates nothing. NOTE: like setEmulatedMedia the override persists on the tab until cleared (rate: 1) — callers own restoration.',
171 },
172 {
173 domain: 'Network',
174 method: 'emulateNetworkConditions',
175 scope: 'tab',
176 output: 'trusted',
177 justification: 'Bandwidth/latency emulation on the active tab, for measuring page behaviour on a slow connection. Constrains traffic rather than reading it — no request bodies, headers or cookies are exposed. NOTE: like setEmulatedMedia the override persists on the tab until cleared (offline: false plus default throughput/latency) — callers own restoration.',
178 },
179 // ─── Page capture (output, not navigation) ─────────────────
180 {
181 domain: 'Page',
182 method: 'captureScreenshot',
183 scope: 'tab',
184 output: 'untrusted',
185 justification: 'Screenshot bytes; output is bounded image data (no marker injection vector).',
186 },
187 {
188 domain: 'Page',
189 method: 'printToPDF',
190 scope: 'tab',
191 output: 'untrusted',
192 justification: 'PDF bytes; bounded binary output.',
193 },
194 // NOTE: Page.navigate is INTENTIONALLY NOT on the allowlist (Codex T2 cat 4).
195 // Use $B goto for navigation; that path goes through the URL blocklist.
196 // ─── Network metadata (NOT bodies/cookies — those exfil data) ──
197 {
198 domain: 'Network',
199 method: 'enable',
200 scope: 'tab',
201 output: 'trusted',
202 justification: 'Domain enable; required prerequisite. Does not return data.',
203 },
204 {
205 domain: 'Network',
206 method: 'disable',
207 scope: 'tab',
208 output: 'trusted',
209 justification: 'Domain disable; mirrors Network.enable for cleanup symmetry.',
210 },
211 // NOTE: Network.getResponseBody, Network.getCookies, Network.replayXHR,
212 // Network.loadNetworkResource are INTENTIONALLY NOT allowed (Codex T2 cat 7).
213 // ─── Runtime (limited, NO evaluate/callFunctionOn) ──────────
214 // Runtime.evaluate/callFunctionOn/compileScript/runScript = RCE if exposed (Codex T2 cat 6).
215 // Only a tiny safe subset:
216 {
217 domain: 'Runtime',
218 method: 'getProperties',
219 scope: 'tab',
220 output: 'untrusted',
221 justification: 'Inspect properties of an existing remote object. Read-only; output may contain page data.',
222 },
223]);
225const CDP_ALLOWLIST_INDEX: Map<string, CdpAllowEntry> = new Map(
226 CDP_ALLOWLIST.map((e) => [`${e.domain}.${e.method}`, e]),
227);
229export function lookupCdpMethod(qualifiedName: string): CdpAllowEntry | null {
230 return CDP_ALLOWLIST_INDEX.get(qualifiedName) ?? null;
233export function isCdpMethodAllowed(qualifiedName: string): boolean {
234 return CDP_ALLOWLIST_INDEX.has(qualifiedName);