11import type { Page } from '@playwright/test'
22
3+ /**
4+ * Sets an input field's value via the DOM, bypassing Playwright's fill() API.
5+ *
6+ * Security (shopify/bugbounty#3638393): Playwright's test runner logs every
7+ * fill() call — including the literal value — into trace files, which are
8+ * uploaded as publicly downloadable CI artifacts. Using evaluate() to set
9+ * the value directly avoids the Playwright action log entirely. The runner's
10+ * tracing instruments at a level above context.tracing, so context.tracing.stop()
11+ * does NOT prevent the leak.
12+ */
13+ async function fillSensitive ( page : Page , selector : string , value : string ) : Promise < void > {
14+ const locator = page . locator ( selector ) . first ( )
15+ await locator . evaluate ( ( el , val ) => {
16+ ; ( el as unknown as { value : string } ) . value = val
17+ el . dispatchEvent ( new Event ( 'input' , { bubbles : true } ) )
18+ el . dispatchEvent ( new Event ( 'change' , { bubbles : true } ) )
19+ } , value )
20+ }
21+
322/**
423 * Completes the Shopify OAuth login flow on a Playwright page.
524 */
@@ -9,12 +28,12 @@ export async function completeLogin(page: Page, loginUrl: string, email: string,
928 try {
1029 // Fill in email
1130 await page . waitForSelector ( 'input[name="account[email]"], input[type="email"]' , { timeout : 60_000 } )
12- await page . locator ( 'input[name="account[email]"], input[type="email"]' ) . first ( ) . fill ( email )
31+ await fillSensitive ( page , 'input[name="account[email]"], input[type="email"]' , email )
1332 await page . locator ( 'button[type="submit"]' ) . first ( ) . click ( )
1433
1534 // Fill in password
1635 await page . waitForSelector ( 'input[name="account[password]"], input[type="password"]' , { timeout : 60_000 } )
17- await page . locator ( 'input[name="account[password]"], input[type="password"]' ) . first ( ) . fill ( password )
36+ await fillSensitive ( page , 'input[name="account[password]"], input[type="password"]' , password )
1837 await page . locator ( 'button[type="submit"]' ) . first ( ) . click ( )
1938
2039 // Handle any confirmation/approval page
@@ -27,12 +46,10 @@ export async function completeLogin(page: Page, loginUrl: string, email: string,
2746 // No confirmation page — expected
2847 }
2948 } catch ( error ) {
30- const pageContent = await page . content ( ) . catch ( ( ) => '(failed to get content)' )
3149 const pageUrl = page . url ( )
32- throw new Error (
33- `Login failed at ${ pageUrl } \n` +
34- `Original error: ${ error } \n` +
35- `Page HTML (first 2000 chars): ${ pageContent . slice ( 0 , 2000 ) } ` ,
36- )
50+ // Clear the page so failure artifacts (screenshots, trace snapshots) do
51+ // not capture the login form with credentials still populated.
52+ await page . goto ( 'about:blank' ) . catch ( ( ) => { } )
53+ throw new Error ( `Login failed at ${ pageUrl } \nOriginal error: ${ error } ` )
3754 }
3855}
0 commit comments