diff --git a/packages/e2e/helpers/browser-login.ts b/packages/e2e/helpers/browser-login.ts index ba637b6d382..da5a4766c50 100644 --- a/packages/e2e/helpers/browser-login.ts +++ b/packages/e2e/helpers/browser-login.ts @@ -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 { + const locator = page.locator(selector).first() + await locator.evaluate((el, val) => { + ;(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. */ @@ -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() // Handle any confirmation/approval page @@ -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}`) } }