Skip to content

Commit 0805e56

Browse files
committed
fail closed on ambiguous captcha frames
1 parent 1a1db76 commit 0805e56

3 files changed

Lines changed: 98 additions & 10 deletions

File tree

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ export function applyCaptchaFrameVisibility(candidates, frameContexts, navigatio
6565
}
6666

6767
const uniqueMatch = (items) => items.length === 1 ? items[0] : null;
68+
const httpOrigin = (value) => {
69+
try {
70+
const parsed = new URL(String(value || ''));
71+
return /^https?:$/.test(parsed.protocol) ? parsed.origin : '';
72+
} catch (_) {
73+
return '';
74+
}
75+
};
6876
const findEmbeddingFrame = (frameId, parentFrameId) => {
6977
const parentContext = contextsByFrameId.get(parentFrameId);
7078
if (!parentContext) return null;
@@ -91,6 +99,13 @@ export function applyCaptchaFrameVisibility(candidates, frameContexts, navigatio
9199
const byName = uniqueMatch(childFrames.filter(child => String(child?.name || '') === frameName));
92100
if (byName) return byName;
93101
}
102+
const frameOrigins = new Set([...frameUrls].map(httpOrigin).filter(Boolean));
103+
if (frameOrigins.size) {
104+
const byOrigin = uniqueMatch(childFrames.filter(child =>
105+
[child?.loadedUrl, child?.url].some(url => frameOrigins.has(httpOrigin(url)))
106+
));
107+
if (byOrigin) return byOrigin;
108+
}
94109
return childFrames.length === 1 ? childFrames[0] : null;
95110
};
96111

@@ -112,12 +127,13 @@ export function applyCaptchaFrameVisibility(candidates, frameContexts, navigatio
112127
const parentVisible = frameIsVisible(parentFrameId, visiting);
113128
visiting.delete(frameId);
114129
const embeddingFrame = parentVisible ? findEmbeddingFrame(frameId, parentFrameId) : null;
115-
// A cross-origin child can redirect while its embedding iframe keeps the
116-
// original src. In that case parent code cannot read loadedUrl, so a
117-
// missing match means visibility is unknown rather than hidden. Only
118-
// demote when a matched embedding frame is conclusively hidden.
130+
// A unique origin match covers common cross-origin redirects whose iframe
131+
// keeps its original src. When multiple embedding elements are still
132+
// indistinguishable, visibility is unknown; fail closed so an internally
133+
// visible widget inside a hidden iframe is not promoted into the paid-task
134+
// ranking tier.
119135
const visible = parentVisible
120-
&& (embeddingFrame ? embeddingFrame.visible === true : true);
136+
&& embeddingFrame?.visible === true;
121137
visibilityByFrameId.set(frameId, visible);
122138
return visible;
123139
};

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ export function applyCaptchaFrameVisibility(candidates, frameContexts, navigatio
6565
}
6666

6767
const uniqueMatch = (items) => items.length === 1 ? items[0] : null;
68+
const httpOrigin = (value) => {
69+
try {
70+
const parsed = new URL(String(value || ''));
71+
return /^https?:$/.test(parsed.protocol) ? parsed.origin : '';
72+
} catch (_) {
73+
return '';
74+
}
75+
};
6876
const findEmbeddingFrame = (frameId, parentFrameId) => {
6977
const parentContext = contextsByFrameId.get(parentFrameId);
7078
if (!parentContext) return null;
@@ -91,6 +99,13 @@ export function applyCaptchaFrameVisibility(candidates, frameContexts, navigatio
9199
const byName = uniqueMatch(childFrames.filter(child => String(child?.name || '') === frameName));
92100
if (byName) return byName;
93101
}
102+
const frameOrigins = new Set([...frameUrls].map(httpOrigin).filter(Boolean));
103+
if (frameOrigins.size) {
104+
const byOrigin = uniqueMatch(childFrames.filter(child =>
105+
[child?.loadedUrl, child?.url].some(url => frameOrigins.has(httpOrigin(url)))
106+
));
107+
if (byOrigin) return byOrigin;
108+
}
94109
return childFrames.length === 1 ? childFrames[0] : null;
95110
};
96111

@@ -112,12 +127,13 @@ export function applyCaptchaFrameVisibility(candidates, frameContexts, navigatio
112127
const parentVisible = frameIsVisible(parentFrameId, visiting);
113128
visiting.delete(frameId);
114129
const embeddingFrame = parentVisible ? findEmbeddingFrame(frameId, parentFrameId) : null;
115-
// A cross-origin child can redirect while its embedding iframe keeps the
116-
// original src. In that case parent code cannot read loadedUrl, so a
117-
// missing match means visibility is unknown rather than hidden. Only
118-
// demote when a matched embedding frame is conclusively hidden.
130+
// A unique origin match covers common cross-origin redirects whose iframe
131+
// keeps its original src. When multiple embedding elements are still
132+
// indistinguishable, visibility is unknown; fail closed so an internally
133+
// visible widget inside a hidden iframe is not promoted into the paid-task
134+
// ranking tier.
119135
const visible = parentVisible
120-
&& (embeddingFrame ? embeddingFrame.visible === true : true);
136+
&& embeddingFrame?.visible === true;
121137
visibilityByFrameId.set(frameId, visible);
122138
return visible;
123139
};

test/run.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51529,6 +51529,62 @@ test('captcha frame visibility propagation demotes descendants of hidden embeddi
5152951529
'KEY_REDIRECTED',
5153051530
`${build}: redirected visible CAPTCHA lost to another candidate`,
5153151531
);
51532+
51533+
const unmatchedOpaqueCandidate = {
51534+
...nestedCandidate,
51535+
frameId: 31,
51536+
frameUrl: 'about:blank',
51537+
websiteKey: 'KEY_UNMATCHED_OPAQUE',
51538+
};
51539+
const unmatchedOpaqueAdjusted = runtime.applyCaptchaFrameVisibility(
51540+
[visibleCandidate, unmatchedOpaqueCandidate],
51541+
[
51542+
{
51543+
frameId: 0,
51544+
frameUrl: visibleCandidate.frameUrl,
51545+
frameName: '',
51546+
childFrames: [
51547+
{
51548+
url: 'about:blank',
51549+
loadedUrl: 'about:blank',
51550+
name: '',
51551+
visible: true,
51552+
},
51553+
{
51554+
url: 'about:blank',
51555+
loadedUrl: 'about:blank',
51556+
name: '',
51557+
visible: false,
51558+
},
51559+
],
51560+
},
51561+
{
51562+
frameId: 31,
51563+
frameUrl: 'about:blank',
51564+
frameName: '',
51565+
childFrames: [],
51566+
},
51567+
],
51568+
[
51569+
{ frameId: 0, parentFrameId: -1, url: visibleCandidate.frameUrl },
51570+
{ frameId: 31, parentFrameId: 0, url: 'about:blank' },
51571+
],
51572+
);
51573+
assert.equal(
51574+
unmatchedOpaqueAdjusted[1].frameVisible,
51575+
false,
51576+
`${build}: unmatched opaque child was promoted to visible`,
51577+
);
51578+
assert.equal(
51579+
unmatchedOpaqueAdjusted[1].visible,
51580+
false,
51581+
`${build}: unmatched opaque CAPTCHA remained active`,
51582+
);
51583+
assert.equal(
51584+
runtime.selectCaptchaCandidate(unmatchedOpaqueAdjusted).selected.websiteKey,
51585+
'KEY_VISIBLE',
51586+
`${build}: unmatched opaque CAPTCHA displaced the proven visible widget`,
51587+
);
5153251588
}
5153351589
});
5153451590

0 commit comments

Comments
 (0)