Skip to content

Commit 4985764

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
Merge remote-tracking branch 'origin/staging' into add-quickbooks-procurement
2 parents d142772 + 4f776b7 commit 4985764

92 files changed

Lines changed: 10473 additions & 426 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/desktop/src/main/browser-agent/page-functions.test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,20 @@ describe('secret-field detection', () => {
140140
],
141141
['new-password field', '<input type="text" autocomplete="new-password" />'],
142142
['uppercase autocomplete token', '<input type="text" autocomplete="Current-Password" />'],
143+
// The spec allows space-separated detail tokens and WebAuthn recommends
144+
// this exact value, so whole-string equality missed it.
145+
[
146+
'WebAuthn multi-token autocomplete',
147+
'<input type="text" autocomplete="current-password webauthn" />',
148+
],
149+
[
150+
'section-scoped autocomplete',
151+
'<input type="text" autocomplete="section-login current-password" />',
152+
],
153+
[
154+
'multi-token new-password with surrounding whitespace',
155+
'<input type="text" autocomplete=" new-password webauthn " />',
156+
],
143157
]
144158

145159
it.each(secretCases)('clickElement refuses a %s', (_label, html) => {
@@ -276,6 +290,24 @@ describe('collectSnapshot', () => {
276290
expect(outline).not.toContain('value=')
277291
})
278292

293+
it.each([
294+
['a one-time code', 'one-time-code', '123456'],
295+
['a card number', 'cc-number', '4111111111111111'],
296+
['a card security code', 'cc-csc', '737'],
297+
['a card expiry', 'cc-exp', '12/29'],
298+
])('withholds the value of %s while still listing the field', (_label, token, value) => {
299+
document.body.innerHTML = `<input type="text" autocomplete="${token}" value="${value}" aria-label="Field" />`
300+
visible(document.querySelector('input') as HTMLInputElement)
301+
302+
const outline = outlineOf(collectSnapshot())
303+
304+
// Not reported as a password-field: the agent must still be able to fill
305+
// these, it just never learns what is already there.
306+
expect(outline).not.toContain('password-field')
307+
expect(outline).not.toContain(value)
308+
expect(outline).toContain('value-withheld')
309+
})
310+
279311
it('withholds the value of a revealed password field', () => {
280312
document.body.innerHTML =
281313
'<input type="text" autocomplete="current-password" value="hunter2" aria-label="Password" />'
@@ -317,6 +349,25 @@ describe('readActiveElementState', () => {
317349
expect(readActiveElementState()).toMatchObject({ redacted: true, valuePreview: '' })
318350
})
319351

352+
it.each([
353+
['a one-time code', 'one-time-code', '123456'],
354+
['a card number', 'cc-number', '4111111111111111'],
355+
['a card security code', 'cc-csc', '737'],
356+
])('withholds %s on readback but still confirms the fill', (_label, token, value) => {
357+
document.body.innerHTML = `<input type="text" autocomplete="${token}" value="${value}" />`
358+
setActiveElement(document, document.querySelector('input'))
359+
360+
// valueLength is kept: without it a successful type reads as "still empty"
361+
// and the agent types the code a second time.
362+
expect(readActiveElementState()).toEqual({
363+
activeElement: 'input',
364+
selectedChars: 0,
365+
valueLength: value.length,
366+
valuePreview: '',
367+
redacted: true,
368+
})
369+
})
370+
320371
it('reports ordinary fields in full', () => {
321372
document.body.innerHTML = '<input type="text" value="tokyo" />'
322373
setActiveElement(document, document.querySelector('input'))
@@ -343,6 +394,35 @@ describe('readActiveElementState', () => {
343394
})
344395
})
345396

397+
describe('XHTML lower-case tagName', () => {
398+
/** An element whose tagName reads lower-case, as it does in an XHTML document. */
399+
function lowerCaseTagInput(html: string): HTMLInputElement {
400+
document.body.innerHTML = html
401+
const input = document.querySelector('input') as HTMLInputElement
402+
Object.defineProperty(input, 'tagName', { configurable: true, get: () => 'input' })
403+
return input
404+
}
405+
406+
it('still refuses a password field whose tagName is lower-case', () => {
407+
const input = lowerCaseTagInput('<input type="password" />')
408+
register(visible(input))
409+
410+
expect(typeIntoElement(0, 'hunter2', false)).toEqual({ error: 'password' })
411+
expect(input.value).toBe('')
412+
})
413+
414+
it('still withholds the value of a lower-case-tagName credential field', () => {
415+
const input = lowerCaseTagInput(
416+
'<input type="password" value="hunter2" aria-label="Password" />'
417+
)
418+
visible(input)
419+
420+
const outline = outlineOf(collectSnapshot())
421+
422+
expect(outline).not.toContain('hunter2')
423+
})
424+
})
425+
346426
describe('activeElementSecrecy', () => {
347427
it('reports safe for an ordinary field', () => {
348428
document.body.innerHTML = '<input type="text" />'
@@ -386,6 +466,46 @@ describe('activeElementSecrecy', () => {
386466
expect(activeElementSecrecy()).toBe('opaque')
387467
})
388468

469+
it('reports opaque for a password field inside a CLOSED shadow root', () => {
470+
const host = document.createElement('div')
471+
document.body.append(host)
472+
const shadow = host.attachShadow({ mode: 'closed' })
473+
shadow.innerHTML = '<input type="password" />'
474+
// Focus inside a closed root retargets to the host and `shadowRoot` reads
475+
// null, which is exactly what the browser reports and what made this 'safe'.
476+
setActiveElement(document, host)
477+
478+
expect(host.shadowRoot).toBeNull()
479+
expect(activeElementSecrecy()).toBe('opaque')
480+
})
481+
482+
it('reports opaque for a closed shadow root on a custom element', () => {
483+
const host = document.createElement('my-login')
484+
document.body.append(host)
485+
host.attachShadow({ mode: 'closed' }).innerHTML = '<input autocomplete="new-password" />'
486+
setActiveElement(document, host)
487+
488+
expect(activeElementSecrecy()).toBe('opaque')
489+
})
490+
491+
it('still reports safe for a focused element that is focusable in its own right', () => {
492+
// The false-positive guard: a div the page made focusable is focused
493+
// itself, not hiding a shadow tree, so keystrokes are not refused.
494+
document.body.innerHTML = '<div tabindex="0">menu</div>'
495+
setActiveElement(document, document.querySelector('div'))
496+
497+
expect(activeElementSecrecy()).toBe('safe')
498+
})
499+
500+
it('still reports safe for a focused contenteditable', () => {
501+
document.body.innerHTML = '<div contenteditable="true">note</div>'
502+
const editable = document.querySelector('div') as HTMLElement
503+
Object.defineProperty(editable, 'isContentEditable', { get: () => true })
504+
setActiveElement(document, editable)
505+
506+
expect(activeElementSecrecy()).toBe('safe')
507+
})
508+
389509
it('descends into a same-origin frame instead of calling it opaque', () => {
390510
const frame = document.createElement('iframe')
391511
document.body.append(frame)

0 commit comments

Comments
 (0)