Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions packages/e2e/helpers/browser-login.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
import type {Page} from '@playwright/test'

/**
* Sets an input field's value via the DOM, bypassing Playwright's fill() API.
*
* Security (shopify/bugbounty#3638393): Playwright's test runner logs every
* fill() call — including the literal value — into trace files, which are
* uploaded as publicly downloadable CI artifacts. Using evaluate() to set
* the value directly avoids the Playwright action log entirely. The runner's
* tracing instruments at a level above context.tracing, so context.tracing.stop()
* does NOT prevent the leak.
*/
async function fillSensitive(page: Page, selector: string, value: string): Promise<void> {
const locator = page.locator(selector).first()
await locator.evaluate((el, val) => {
Comment thread
dmerand marked this conversation as resolved.
;(el as unknown as {value: string}).value = val
el.dispatchEvent(new Event('input', {bubbles: true}))
el.dispatchEvent(new Event('change', {bubbles: true}))
}, value)
}

/**
* Completes the Shopify OAuth login flow on a Playwright page.
*/
Expand All @@ -9,12 +28,12 @@ export async function completeLogin(page: Page, loginUrl: string, email: string,
try {
// Fill in email
await page.waitForSelector('input[name="account[email]"], input[type="email"]', {timeout: 60_000})
await page.locator('input[name="account[email]"], input[type="email"]').first().fill(email)
await fillSensitive(page, 'input[name="account[email]"], input[type="email"]', email)
await page.locator('button[type="submit"]').first().click()

// Fill in password
await page.waitForSelector('input[name="account[password]"], input[type="password"]', {timeout: 60_000})
await page.locator('input[name="account[password]"], input[type="password"]').first().fill(password)
await fillSensitive(page, 'input[name="account[password]"], input[type="password"]', password)
await page.locator('button[type="submit"]').first().click()
Comment thread
dmerand marked this conversation as resolved.

// Handle any confirmation/approval page
Expand All @@ -27,12 +46,10 @@ export async function completeLogin(page: Page, loginUrl: string, email: string,
// No confirmation page — expected
}
} catch (error) {
const pageContent = await page.content().catch(() => '(failed to get content)')
const pageUrl = page.url()
throw new Error(
`Login failed at ${pageUrl}\n` +
`Original error: ${error}\n` +
`Page HTML (first 2000 chars): ${pageContent.slice(0, 2000)}`,
)
// Clear the page so failure artifacts (screenshots, trace snapshots) do
// not capture the login form with credentials still populated.
await page.goto('about:blank').catch(() => {})
throw new Error(`Login failed at ${pageUrl}\nOriginal error: ${error}`)
}
}
Loading