Skip to content

Commit 33fbf7b

Browse files
authored
Merge pull request #506 from esokullu/main
Fix and enhance reCAPTCHA enterprise functionality
2 parents 9a55567 + 63b3ab2 commit 33fbf7b

14 files changed

Lines changed: 3291 additions & 66 deletions

src/chrome/src/agent/agent.js

Lines changed: 613 additions & 10 deletions
Large diffs are not rendered by default.

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

Lines changed: 91 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,24 @@ export function applyCaptchaFrameVisibility(candidates, frameContexts, navigatio
137137
visibilityByFrameId.set(frameId, visible);
138138
return visible;
139139
};
140+
const dialogAssociationByFrameId = new Map();
141+
const frameIsDialogAssociated = (frameId, visiting = new Set()) => {
142+
if (dialogAssociationByFrameId.has(frameId)) {
143+
return dialogAssociationByFrameId.get(frameId);
144+
}
145+
if (!Number.isInteger(frameId) || frameId === 0 || visiting.has(frameId)) {
146+
return false;
147+
}
148+
const parentFrameId = navigationByFrameId.get(frameId)?.parentFrameId;
149+
if (!Number.isInteger(parentFrameId) || parentFrameId === -1) return false;
150+
const embeddingFrame = findEmbeddingFrame(frameId, parentFrameId);
151+
visiting.add(frameId);
152+
const associated = embeddingFrame?.dialogAssociated === true
153+
|| frameIsDialogAssociated(parentFrameId, visiting);
154+
visiting.delete(frameId);
155+
dialogAssociationByFrameId.set(frameId, associated);
156+
return associated;
157+
};
140158

141159
const sourceCandidates = Array.isArray(candidates) ? candidates : [];
142160
const pathIsStrictAncestor = (sourcePath, targetPath) => {
@@ -249,6 +267,8 @@ export function applyCaptchaFrameVisibility(candidates, frameContexts, navigatio
249267
...candidate,
250268
frameVisible,
251269
websiteURL: nearestHttpUrl(candidate),
270+
dialogAssociated: candidate?.dialogAssociated === true
271+
|| frameIsDialogAssociated(candidate?.frameId),
252272
visible: candidate?.visible === true && frameVisible,
253273
normalCheckbox: candidate?.normalCheckbox === true && candidate?.visible === true && frameVisible,
254274
};
@@ -271,6 +291,7 @@ function candidateSummary(candidate) {
271291
visible: candidate?.visible === true,
272292
normalCheckbox: candidate?.normalCheckbox === true,
273293
challengeFrame: candidate?.challengeFrame === true,
294+
dialogAssociated: candidate?.dialogAssociated === true,
274295
frameVisible: candidate?.frameVisible !== false,
275296
isInvisible: candidate?.isInvisible === true,
276297
isEnterprise: candidate?.isEnterprise === true,
@@ -384,6 +405,7 @@ export function selectCaptchaCandidate(candidates, constraints = {}) {
384405
visible: previous.visible === true || candidate.visible === true,
385406
normalCheckbox: previous.normalCheckbox === true || candidate.normalCheckbox === true,
386407
challengeFrame: previous.challengeFrame === true || candidate.challengeFrame === true,
408+
dialogAssociated: previous.dialogAssociated === true || candidate.dialogAssociated === true,
387409
responseField: previous.responseField === true || candidate.responseField === true,
388410
};
389411
for (const field of taskParameterFields) {
@@ -622,20 +644,67 @@ export function detectCaptchaCandidatesInPage(scope = null) {
622644
return false;
623645
}
624646
};
625-
const add = (candidate) => {
647+
const challengeDialogRe = /\b(?:(?:re|h|fun)?captcha|security verification|human verification|verify (?:that )?you(?:'|\u2019)re (?:a )?human|verify (?:that )?you are (?:a )?human|are you (?:a )?human|robot check|challenge verification)\b/i;
648+
const challengeDialogs = Array.from(
649+
pageDocument.querySelectorAll('dialog, [role="dialog"], [role="alertdialog"]')
650+
).filter((element) => {
651+
if (!visibleElement(element)) return false;
652+
let labelledBy = '';
653+
try {
654+
labelledBy = String(element.getAttribute?.('aria-labelledby') || '')
655+
.split(/\s+/)
656+
.filter(Boolean)
657+
.map(id => pageDocument.getElementById?.(id)?.textContent || '')
658+
.join(' ');
659+
} catch (_) {}
660+
return [
661+
element.getAttribute?.('aria-label'),
662+
labelledBy,
663+
element.querySelector?.('h1, h2, h3, [role="heading"]')?.textContent,
664+
element.getAttribute?.('title'),
665+
element.innerText,
666+
element.textContent,
667+
].some(value => challengeDialogRe.test(String(value || '')));
668+
});
669+
const elementInChallengeDialog = (element) => {
670+
if (!element) return false;
671+
return challengeDialogs.some((dialog) => {
672+
if (dialog === element) return true;
673+
try {
674+
if (typeof dialog.contains === 'function' && dialog.contains(element)) return true;
675+
} catch (_) {}
676+
let ancestor = element.parentElement || null;
677+
for (let depth = 0; ancestor && depth < 20; depth += 1) {
678+
if (ancestor === dialog) return true;
679+
ancestor = ancestor.parentElement || null;
680+
}
681+
return false;
682+
});
683+
};
684+
const add = (candidate, associationElement = null) => {
626685
if (!candidate?.type) return;
686+
const {
687+
responseFieldDialogAssociated,
688+
alsoResponseFieldDialogAssociated,
689+
...serializableCandidate
690+
} = candidate;
627691
candidates.push({
628-
...candidate,
692+
...serializableCandidate,
629693
frameUrl,
630694
challengeFrame,
631695
responseField,
632696
documentTimeOrigin,
697+
dialogAssociated: candidate.dialogAssociated === true
698+
|| responseFieldDialogAssociated === true
699+
|| alsoResponseFieldDialogAssociated === true
700+
|| elementInChallengeDialog(associationElement),
633701
});
634702
};
635703
const scriptElements = Array.from(pageDocument.querySelectorAll('script[src]'));
636-
const scriptUrls = scriptElements.map(element => {
637-
try { return element.src || ''; } catch (_) { return ''; }
638-
}).filter(Boolean);
704+
const scriptRecords = scriptElements.map((element) => {
705+
try { return { element, url: element.src || '' }; } catch (_) { return { element, url: '' }; }
706+
}).filter(record => record.url);
707+
const scriptUrls = scriptRecords.map(record => record.url);
639708
const responseFieldIdentity = (widget, name, fallbackIndex, widgetCount) => {
640709
const selector = `textarea[name="${name}"], input[name="${name}"]`;
641710
const fields = Array.from(pageDocument.querySelectorAll(selector));
@@ -662,6 +731,7 @@ export function detectCaptchaCandidatesInPage(scope = null) {
662731
return {
663732
...(responseFieldId ? { responseFieldId } : {}),
664733
...(responseFieldIndex >= 0 ? { responseFieldIndex } : {}),
734+
...(elementInChallengeDialog(field) ? { responseFieldDialogAssociated: true } : {}),
665735
};
666736
};
667737
const alsoResponseFieldIdentity = (widget, name, fallbackIndex, widgetCount) => {
@@ -671,8 +741,14 @@ export function detectCaptchaCandidatesInPage(scope = null) {
671741
...(Number.isInteger(identity.responseFieldIndex)
672742
? { alsoResponseFieldIndex: identity.responseFieldIndex }
673743
: {}),
744+
...(identity.responseFieldDialogAssociated
745+
? { alsoResponseFieldDialogAssociated: true }
746+
: {}),
674747
};
675748
};
749+
const recaptchaResponseInChallengeDialog = Array.from(pageDocument.querySelectorAll(
750+
'textarea[name="g-recaptcha-response"], input[name="g-recaptcha-response"]'
751+
)).some(elementInChallengeDialog);
676752

677753
const hcaptchaHosts = Array.from(pageDocument.querySelectorAll(
678754
'.h-captcha[data-sitekey], div[data-hcaptcha-widget-id]'
@@ -691,7 +767,7 @@ export function detectCaptchaCandidatesInPage(scope = null) {
691767
...responseFieldIdentity(host, 'h-captcha-response', widgetIndex, hcaptchaHosts.length),
692768
...alsoResponseFieldIdentity(host, 'g-recaptcha-response', widgetIndex, hcaptchaHosts.length),
693769
detectedVia: 'host',
694-
});
770+
}, host);
695771
}
696772

697773
const turnstileHosts = Array.from(pageDocument.querySelectorAll(
@@ -708,7 +784,7 @@ export function detectCaptchaCandidatesInPage(scope = null) {
708784
callbackName: host.getAttribute('data-callback') || null,
709785
...responseFieldIdentity(host, 'cf-turnstile-response', widgetIndex, turnstileHosts.length),
710786
detectedVia: 'host',
711-
});
787+
}, host);
712788
}
713789

714790
const recaptchaHosts = Array.from(pageDocument.querySelectorAll(
@@ -751,7 +827,7 @@ export function detectCaptchaCandidatesInPage(scope = null) {
751827
: {}),
752828
...responseFieldIdentity(host, 'g-recaptcha-response', widgetIndex, recaptchaHosts.length),
753829
detectedVia: 'host',
754-
});
830+
}, host);
755831
}
756832

757833
const allIframeElements = Array.from(pageDocument.querySelectorAll('iframe'));
@@ -791,7 +867,7 @@ export function detectCaptchaCandidatesInPage(scope = null) {
791867
hcaptchaFrames.length,
792868
),
793869
detectedVia: 'url',
794-
});
870+
}, element);
795871
}
796872
continue;
797873
}
@@ -810,7 +886,7 @@ export function detectCaptchaCandidatesInPage(scope = null) {
810886
turnstileFrames.length,
811887
),
812888
detectedVia: 'url',
813-
});
889+
}, element);
814890
}
815891
continue;
816892
}
@@ -846,10 +922,10 @@ export function detectCaptchaCandidatesInPage(scope = null) {
846922
recaptchaFrames.length,
847923
),
848924
detectedVia: 'url',
849-
});
925+
}, element);
850926
}
851927

852-
for (const url of scriptUrls) {
928+
for (const { element, url } of scriptRecords) {
853929
if (!/recaptcha\/(api\.js|enterprise\.js)/i.test(url)) continue;
854930
const websiteKey = urlParam(url, 'render');
855931
if (!websiteKey || websiteKey === 'explicit') continue;
@@ -862,9 +938,10 @@ export function detectCaptchaCandidatesInPage(scope = null) {
862938
isEnterprise,
863939
visible: false,
864940
normalCheckbox: false,
941+
dialogAssociated: recaptchaResponseInChallengeDialog,
865942
...(pageAction ? { pageAction } : { note: V3_NO_ACTION_NOTE }),
866943
detectedVia: 'script',
867-
});
944+
}, element);
868945
}
869946

870947
const hasDetectedTurnstile = candidates.some(candidate => candidate.type === 'turnstile');
@@ -894,6 +971,7 @@ export function detectCaptchaCandidatesInPage(scope = null) {
894971
loadedUrl,
895972
name,
896973
visible: visibleElement(element),
974+
dialogAssociated: elementInChallengeDialog(element),
897975
};
898976
});
899977
let frameName = '';

0 commit comments

Comments
 (0)