Skip to content

Commit 1a1db76

Browse files
committed
fix opaque captcha targeting
1 parent 040633a commit 1a1db76

7 files changed

Lines changed: 358 additions & 16 deletions

File tree

src/chrome/src/agent/agent.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13823,6 +13823,8 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
1382313823
type,
1382413824
websiteKey,
1382513825
frameUrl,
13826+
frameId,
13827+
framePath,
1382613828
isInvisible,
1382713829
isEnterprise,
1382813830
pageAction,
@@ -13839,7 +13841,13 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
1383913841
if (type !== 'image_to_text') {
1384013842
let detection = null;
1384113843
try {
13842-
detection = await detectCaptcha(tabId, { type, frameUrl, websiteKey });
13844+
detection = await detectCaptcha(tabId, {
13845+
type,
13846+
frameUrl,
13847+
frameId,
13848+
framePath,
13849+
websiteKey,
13850+
});
1384313851
} catch (detectionError) {
1384413852
const explicitTokenOnlyFallback = args?.inject === false && !!type && !!websiteKey;
1384513853
if (!explicitTokenOnlyFallback) {
@@ -13851,7 +13859,11 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
1385113859
}
1385213860
if (detection?.error) {
1385313861
const hasDetectedCandidates = Array.isArray(detection.candidates) && detection.candidates.length > 0;
13854-
const needsDetection = !type || !websiteKey || !!frameUrl;
13862+
const needsDetection = !type
13863+
|| !websiteKey
13864+
|| !!frameUrl
13865+
|| Number.isInteger(frameId)
13866+
|| Array.isArray(framePath);
1385513867
const explicitTokenOnlyFallback = args?.inject === false
1385613868
&& !!type
1385713869
&& !!websiteKey
@@ -13899,7 +13911,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
1389913911
);
1390013912
}
1390113913
type = type || detected.type;
13902-
websiteURL = captchaWebsiteUrl(detected.frameUrl, websiteURL);
13914+
websiteURL = detected.websiteURL || captchaWebsiteUrl(detected.frameUrl, websiteURL);
1390313915
frameUrl = detected.frameUrl || frameUrl;
1390413916
detectionNote = detected.note || null;
1390513917
if (!websiteKey) websiteKey = detected.websiteKey;

src/chrome/src/agent/captcha-frame-runtime.js

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,29 @@ export function applyCaptchaFrameVisibility(candidates, frameContexts, navigatio
201201
ancestorLoaderFrameId: loader.frameId,
202202
};
203203
};
204+
const isHttpUrl = value => /^https?:\/\//i.test(String(value || ''));
205+
const nearestHttpUrl = (candidate) => {
206+
if (isHttpUrl(candidate?.frameUrl)) return String(candidate.frameUrl);
207+
const framePath = Array.isArray(candidate?.framePath) ? candidate.framePath : [];
208+
for (let index = framePath.length - 2; index >= 0; index -= 1) {
209+
if (isHttpUrl(framePath[index]?.frameUrl)) return String(framePath[index].frameUrl);
210+
}
211+
const baseFrameUrl = contextsByFrameId.get(candidate?.frameId)?.frameUrl
212+
|| navigationByFrameId.get(candidate?.frameId)?.url;
213+
if (isHttpUrl(baseFrameUrl)) return String(baseFrameUrl);
214+
let currentFrameId = candidate?.frameId;
215+
const visited = new Set();
216+
while (Number.isInteger(currentFrameId) && !visited.has(currentFrameId)) {
217+
visited.add(currentFrameId);
218+
const parentFrameId = navigationByFrameId.get(currentFrameId)?.parentFrameId;
219+
if (!Number.isInteger(parentFrameId) || parentFrameId === -1) break;
220+
const parentUrl = contextsByFrameId.get(parentFrameId)?.frameUrl
221+
|| navigationByFrameId.get(parentFrameId)?.url;
222+
if (isHttpUrl(parentUrl)) return String(parentUrl);
223+
currentFrameId = parentFrameId;
224+
}
225+
return null;
226+
};
204227

205228
return sourceCandidates.map(rawCandidate => {
206229
const candidate = reconcileAncestorLoader(rawCandidate);
@@ -209,6 +232,7 @@ export function applyCaptchaFrameVisibility(candidates, frameContexts, navigatio
209232
return {
210233
...candidate,
211234
frameVisible,
235+
websiteURL: nearestHttpUrl(candidate),
212236
visible: candidate?.visible === true && frameVisible,
213237
normalCheckbox: candidate?.normalCheckbox === true && candidate?.visible === true && frameVisible,
214238
};
@@ -221,7 +245,11 @@ function candidateSummary(candidate) {
221245
...(Array.isArray(candidate?.framePath) && candidate.framePath.length
222246
? { framePath: candidate.framePath }
223247
: {}),
248+
framePathIndexes: Array.isArray(candidate?.framePath)
249+
? candidate.framePath.map(segment => Number(segment?.index))
250+
: [],
224251
frameUrl: candidate?.frameUrl || '',
252+
websiteURL: candidate?.websiteURL || null,
225253
type: candidate?.type || '',
226254
websiteKey: candidate?.websiteKey || null,
227255
visible: candidate?.visible === true,
@@ -364,6 +392,47 @@ export function selectCaptchaCandidate(candidates, constraints = {}) {
364392
}
365393

366394
let pool = unique;
395+
const requestedFrameId = Number.isInteger(constraints.frameId) ? constraints.frameId : null;
396+
if (requestedFrameId != null) {
397+
pool = pool.filter(candidate => candidate.frameId === requestedFrameId);
398+
if (!pool.length) {
399+
return {
400+
selected: null,
401+
ambiguous: false,
402+
error: `No CAPTCHA candidate was found in frameId ${requestedFrameId}.`,
403+
candidates: unique.map(candidateSummary),
404+
};
405+
}
406+
}
407+
const hasRequestedFramePath = Array.isArray(constraints.framePath);
408+
const requestedFramePath = hasRequestedFramePath
409+
? constraints.framePath.map(value => Number(value))
410+
: [];
411+
if (hasRequestedFramePath) {
412+
if (requestedFramePath.some(value => !Number.isInteger(value) || value < 0)) {
413+
return {
414+
selected: null,
415+
ambiguous: false,
416+
error: 'framePath must be an array of non-negative iframe indexes.',
417+
candidates: unique.map(candidateSummary),
418+
};
419+
}
420+
pool = pool.filter(candidate => {
421+
const candidatePath = Array.isArray(candidate.framePath)
422+
? candidate.framePath.map(segment => Number(segment?.index))
423+
: [];
424+
return candidatePath.length === requestedFramePath.length
425+
&& candidatePath.every((value, index) => value === requestedFramePath[index]);
426+
});
427+
if (!pool.length) {
428+
return {
429+
selected: null,
430+
ambiguous: false,
431+
error: `No CAPTCHA candidate was found at framePath [${requestedFramePath.join(', ')}].`,
432+
candidates: unique.map(candidateSummary),
433+
};
434+
}
435+
}
367436
const requestedFrameUrl = String(constraints.frameUrl || '');
368437
const requestedWebsiteKey = String(constraints.websiteKey || '');
369438
const requestedType = normalizeCaptchaType(constraints.type);
@@ -412,11 +481,30 @@ export function selectCaptchaCandidate(candidates, constraints = {}) {
412481
const topScore = ranked[0].score;
413482
const top = ranked.filter(entry => entry.score === topScore);
414483
if (top.length > 1) {
484+
const summaries = top.map(entry => candidateSummary(entry.candidate));
485+
const uniqueCount = selector => new Set(summaries.map(selector)).size;
486+
const discriminators = [];
487+
if (uniqueCount(candidate => candidate.frameUrl) > 1) discriminators.push('frameUrl');
488+
if (uniqueCount(candidate => candidate.websiteKey) > 1) discriminators.push('websiteKey');
489+
if (uniqueCount(candidate => candidate.frameId) > 1) discriminators.push('frameId');
490+
if (uniqueCount(candidate => JSON.stringify(candidate.framePathIndexes)) > 1) {
491+
discriminators.push('framePath');
492+
}
493+
const finalDiscriminator = discriminators.pop();
494+
const discriminatorText = finalDiscriminator
495+
? discriminators.length === 0
496+
? finalDiscriminator
497+
: discriminators.length === 1
498+
? `${discriminators[0]} or ${finalDiscriminator}`
499+
: `${discriminators.join(', ')}, or ${finalDiscriminator}`
500+
: null;
415501
return {
416502
selected: null,
417503
ambiguous: true,
418-
error: 'Multiple CAPTCHA candidates are equally active. Pass an exact frameUrl or websiteKey to select one.',
419-
candidates: top.map(entry => candidateSummary(entry.candidate)),
504+
error: discriminatorText
505+
? `Multiple CAPTCHA candidates are equally active. Pass an exact ${discriminatorText} from the candidates to select one.`
506+
: 'Multiple CAPTCHA candidates are equally active and expose no unique frame discriminator. Wait for one widget to become inactive before retrying.',
507+
candidates: summaries,
420508
};
421509
}
422510
const selectedConflicts = Array.isArray(top[0].candidate.parameterConflicts)
@@ -446,6 +534,8 @@ export function selectCaptchaCandidate(candidates, constraints = {}) {
446534

447535
function selectedReason(candidate, constraints) {
448536
if (candidate.explicitWebsiteKey) return 'explicit site key for detected Turnstile challenge';
537+
if (Array.isArray(constraints.framePath)) return 'exact framePath match';
538+
if (Number.isInteger(constraints.frameId)) return 'exact frameId match';
449539
if (constraints.frameUrl) return 'exact frameUrl match';
450540
if (constraints.websiteKey) return 'exact websiteKey match';
451541
if (candidate.normalCheckbox && candidate.visible) return 'visible checkbox challenge';

src/chrome/src/agent/tools.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1054,7 +1054,7 @@ export const AGENT_TOOLS = [
10541054
type: 'function',
10551055
function: {
10561056
name: 'solve_captcha',
1057-
description: 'Solve a CAPTCHA on the current page using the CapSolver API (only available when the user has enabled CapSolver and provided an API key in Settings → General → Advanced). The tool scans every frame, ranks the active visible widget above hidden/background integrations, and fills missing type/site-key parameters from the selected frame. Returns the token, selected frame/type, and targeted injection/callback status; verify page progress afterward because token injection alone does not prove the challenge cleared. On failure (no CapSolver key configured, ambiguous candidates, unknown type, API error, timeout) the tool returns `{ success: false, error: "..." }` — use an exact frameUrl/websiteKey when offered, otherwise ask the user to solve it manually.',
1057+
description: 'Solve a CAPTCHA on the current page using the CapSolver API (only available when the user has enabled CapSolver and provided an API key in Settings → General → Advanced). The tool scans every frame, ranks the active visible widget above hidden/background integrations, and fills missing type/site-key parameters from the selected frame. Returns the token, selected frame/type, and targeted injection/callback status; verify page progress afterward because token injection alone does not prove the challenge cleared. On failure (no CapSolver key configured, ambiguous candidates, unknown type, API error, timeout) the tool returns `{ success: false, error: "..." }` — use only the exact frameUrl, websiteKey, frameId, or framePath discriminator offered by the ambiguity result; otherwise ask the user to solve it manually.',
10581058
parameters: {
10591059
type: 'object',
10601060
properties: {
@@ -1065,6 +1065,12 @@ export const AGENT_TOOLS = [
10651065
},
10661066
websiteKey: { type: 'string', description: 'Site key from the captcha widget\'s data-sitekey attribute. Auto-detected when omitted.' },
10671067
frameUrl: { type: 'string', description: 'Exact URL of the frame containing the intended CAPTCHA. Usually omit; use a candidate URL returned by an ambiguity error to select among equally active widgets.' },
1068+
frameId: { type: 'integer', description: 'Exact browser frame ID from an ambiguity result. Use when tied candidates share the same frameUrl and site key.' },
1069+
framePath: {
1070+
type: 'array',
1071+
items: { type: 'integer', minimum: 0 },
1072+
description: 'Exact iframe-index path from a candidate\'s framePathIndexes diagnostic. Use with frameId when inherited-origin candidates share the same URL and site key.',
1073+
},
10681074
isInvisible: { type: 'boolean', description: 'reCAPTCHA v2 / hCaptcha only — true when the widget uses invisible mode (no visible checkbox). Auto-detected when omitted.' },
10691075
isEnterprise: { type: 'boolean', description: 'reCAPTCHA v2/v3 only — true when the widget uses Google reCAPTCHA Enterprise. Auto-detected when omitted.' },
10701076
pageAction: { type: 'string', description: 'Required for reCAPTCHA v3 — the action name the page uses (e.g. "login", "submit"). Auto-detected from data-action or the loader script when present; pass it explicitly if detection reports it missing.' },

src/firefox/src/agent/agent.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12101,6 +12101,8 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
1210112101
type,
1210212102
websiteKey,
1210312103
frameUrl,
12104+
frameId,
12105+
framePath,
1210412106
isInvisible,
1210512107
isEnterprise,
1210612108
pageAction,
@@ -12117,7 +12119,13 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
1211712119
if (type !== 'image_to_text') {
1211812120
let detection = null;
1211912121
try {
12120-
detection = await detectCaptcha(tabId, { type, frameUrl, websiteKey });
12122+
detection = await detectCaptcha(tabId, {
12123+
type,
12124+
frameUrl,
12125+
frameId,
12126+
framePath,
12127+
websiteKey,
12128+
});
1212112129
} catch (detectionError) {
1212212130
const explicitTokenOnlyFallback = args?.inject === false && !!type && !!websiteKey;
1212312131
if (!explicitTokenOnlyFallback) {
@@ -12129,7 +12137,11 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
1212912137
}
1213012138
if (detection?.error) {
1213112139
const hasDetectedCandidates = Array.isArray(detection.candidates) && detection.candidates.length > 0;
12132-
const needsDetection = !type || !websiteKey || !!frameUrl;
12140+
const needsDetection = !type
12141+
|| !websiteKey
12142+
|| !!frameUrl
12143+
|| Number.isInteger(frameId)
12144+
|| Array.isArray(framePath);
1213312145
const explicitTokenOnlyFallback = args?.inject === false
1213412146
&& !!type
1213512147
&& !!websiteKey
@@ -12177,7 +12189,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
1217712189
);
1217812190
}
1217912191
type = type || detected.type;
12180-
websiteURL = captchaWebsiteUrl(detected.frameUrl, websiteURL);
12192+
websiteURL = detected.websiteURL || captchaWebsiteUrl(detected.frameUrl, websiteURL);
1218112193
frameUrl = detected.frameUrl || frameUrl;
1218212194
detectionNote = detected.note || null;
1218312195
if (!websiteKey) websiteKey = detected.websiteKey;

0 commit comments

Comments
 (0)