Skip to content

Commit 10645fc

Browse files
authored
Merge pull request #7191 from Shopify/e2e-scrub-credentials-from-traces
Stop Playwright tracing during E2E login to prevent credential exposure
2 parents b576023 + cf2cc00 commit 10645fc

1 file changed

Lines changed: 25 additions & 8 deletions

File tree

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
import 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

Comments
 (0)