Skip to content
Open
Show file tree
Hide file tree
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
4 changes: 1 addition & 3 deletions e2e/davinci-app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
<a href="https://www.typescriptlang.org/" target="_blank">
<img src="./public/typescript.svg" class="logo vanilla" alt="TypeScript logo" />
</a>
<form id="form">
<div class="error-div"></div>
</form>
<form id="form"></form>
</div>
</div>
<script type="module" src="main.ts"></script>
Expand Down
10 changes: 5 additions & 5 deletions e2e/davinci-app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@ const urlParams = new URLSearchParams(window.location.search);
formEl.innerHTML = '';

const clientInfo = davinciClient.getClient();
//const clientInfo = node.client;

let formName = '';

Expand All @@ -191,11 +190,12 @@ const urlParams = new URLSearchParams(window.location.search);

const error = davinciClient.getError();
if (error) {
formEl.appendChild(document.createElement('div')).setAttribute('id', 'error-div');
const errorDiv = formEl.querySelector('#error-div');
if (errorDiv && clientInfo?.status === 'continue') {
const errorDiv = document.createElement('div');
formEl.appendChild(errorDiv).setAttribute('id', 'error-div');
if (errorDiv && clientInfo?.status === 'error') {
errorDiv.style.color = 'red';
errorDiv.innerHTML = `
<div>${davinciClient.getError()?.message}</div>
<p><strong>Error</strong>: ${davinciClient.getError()?.message}</p>
`;
}
Comment on lines +193 to 200
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Potential XSS vulnerability and unnecessary DOM element creation.

Two concerns:

  1. XSS Risk: The error message is inserted via innerHTML without sanitization. If davinciClient.getError()?.message can contain user-controlled input, this creates an XSS vulnerability.

  2. Empty Element: The errorDiv is always appended to the DOM (Line 194) but only populated when status === 'error' (Line 195). This leaves an empty div#error-div in the DOM for non-error states.

🔒 Proposed fix to address both concerns
     const error = davinciClient.getError();
-    if (error) {
+    if (error && clientInfo?.status === 'error') {
       const errorDiv = document.createElement('div');
       formEl.appendChild(errorDiv).setAttribute('id', 'error-div');
-      if (errorDiv && clientInfo?.status === 'error') {
-        errorDiv.style.color = 'red';
-        errorDiv.innerHTML = `
-          <p><strong>Error</strong>: ${davinciClient.getError()?.message}</p>
-        `;
-      }
+      errorDiv.style.color = 'red';
+      
+      const paragraph = document.createElement('p');
+      const strong = document.createElement('strong');
+      strong.textContent = 'Error';
+      paragraph.appendChild(strong);
+      paragraph.appendChild(document.createTextNode(': ' + (error.message || '')));
+      errorDiv.appendChild(paragraph);
     }

This fix:

  • Only creates the error div when actually needed
  • Uses textContent instead of innerHTML to prevent XSS
🧰 Tools
🪛 ast-grep (0.42.2)

[warning] 196-198: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: errorDiv.innerHTML = <p><strong>Error</strong>: ${davinciClient.getError()?.message}</p>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 196-198: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: errorDiv.innerHTML = <p><strong>Error</strong>: ${davinciClient.getError()?.message}</p>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/davinci-app/main.ts` around lines 193 - 200, The current code always
appends a div via formEl.appendChild(...) and uses innerHTML to render
davinciClient.getError()?.message, causing an unnecessary empty `#error-div` when
no error and an XSS risk; change the logic to only create/append the error
element when clientInfo?.status === 'error' and set its content with textContent
(or equivalent safe API) using davinciClient.getError()?.message to avoid HTML
injection, and remove the unconditional append so no empty div remains when not
in error state.

}
Expand Down
12 changes: 12 additions & 0 deletions e2e/davinci-app/server-configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,16 @@ export const serverConfigs: Record<string, DaVinciConfig> = {
'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration',
},
},
/**
* Polling
*/
'31a587ce-9aa4-4f36-a09f-78cd8a0a74a0': {
clientId: '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0',
redirectUri: window.location.origin + '/',
scope: 'openid profile email revoke',
serverConfig: {
wellknown:
'https://auth.pingone.ca/356a254c-cba3-4ade-be1a-860136e8df01/as/.well-known/openid-configuration',
},
},
};
4 changes: 4 additions & 0 deletions e2e/davinci-app/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ input {
color: #888;
}

.error {
color: red;
}

button {
border-radius: 8px;
border: 1px solid transparent;
Expand Down
178 changes: 178 additions & 0 deletions e2e/davinci-suites/src/polling.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
import { expect, test } from '@playwright/test';
import { asyncEvents } from './utils/async-events.js';

test.describe('Challenge Polling', () => {
test('should succeed when opening magic link', async ({ page, browser }) => {
const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0';
const davinciPolicy = 'f40b544a4dfb575daa0cf5e9487c206a';
const { navigate } = asyncEvents(page);
await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`);

await expect(page.url()).toBe(
`http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`,
);

await page.getByRole('button', { name: 'Sign On' }).click();
await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible();

// Get magic link
const linkLocator = page.getByText('Number Challenge https://auth.pingone');
await expect(linkLocator).toBeVisible();

const linkLocatorText = await linkLocator.innerText();
const magicLink = linkLocatorText.split('Number Challenge ')[1];
expect(magicLink.startsWith('https://auth.pingone'));

// Start polling
await page.getByRole('button', { name: 'Start polling' }).click();
await expect(page.getByText('Polling...')).toBeVisible();

// Go to magic link in another browser to complete challenge
const newContext = await browser.newContext();
const newPage = await newContext.newPage();
await newPage.goto(magicLink);
await expect(newPage.getByText('Close me')).toBeVisible();
await newContext.close();

// Check for success
await expect(page.getByText('Message: approved')).toBeVisible();
});

test('should timeout when retries are exhausted', async ({ page }) => {
const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0';
const davinciPolicy = 'f40b544a4dfb575daa0cf5e9487c206a';
const { navigate } = asyncEvents(page);
await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`);

await expect(page.url()).toBe(
`http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`,
);

await page.getByRole('button', { name: 'Sign On' }).click();
await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible();

// Track poll retries
let numPollRequests = 0;
page.on('request', (request) => {
const method = request.method();
const requestUrl = request.url();

if (method === 'POST' && requestUrl.includes('/status')) {
numPollRequests++;
}
});

// Start polling
await page.getByRole('button', { name: 'Start polling' }).click();
await expect(page.getByText('Polling...')).toBeVisible();

// Wait for timeout
const pollInterval = 2000; // milliseconds
const maxRetries = 5;
await expect(page.getByText('Error: timedOut')).toBeVisible({
timeout: 2 * pollInterval * maxRetries,
});

// Check max retry count
expect(numPollRequests === maxRetries);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Incorrect assertion - test will always pass.

The assertion expect(numPollRequests === maxRetries) evaluates to a boolean and passes it to expect(), which will always succeed. This doesn't actually assert the condition.

🐛 Proposed fix
     // Check max retry count
-    expect(numPollRequests === maxRetries);
+    expect(numPollRequests).toBe(maxRetries);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(numPollRequests === maxRetries);
expect(numPollRequests).toBe(maxRetries);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/davinci-suites/src/polling.test.ts` at line 83, The test's assertion
passes a boolean to expect causing it to always succeed; change the assertion to
use a Jest matcher on the actual value (e.g., replace the current
expect(numPollRequests === maxRetries) with an assertion like
expect(numPollRequests).toBe(maxRetries) or
expect(numPollRequests).toEqual(maxRetries)) so the test actually verifies that
numPollRequests equals maxRetries (refer to the variables numPollRequests and
maxRetries in polling.test.ts).

});

test('should return expired status after challenge expires', async ({ page }) => {
const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0';
const davinciPolicy = 'f40b544a4dfb575daa0cf5e9487c206a';
const { navigate } = asyncEvents(page);
await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`);

await expect(page.url()).toBe(
`http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`,
);

await page.getByRole('button', { name: 'Sign On' }).click();
await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible();

// Track poll retries
let numPollRequests = 0;
page.on('request', (request) => {
const method = request.method();
const requestUrl = request.url();

if (method === 'POST' && requestUrl.includes('/status')) {
numPollRequests++;
}
});

// Wait for challenge to expire
const challengeExpiry = 15000; // milliseconds
await page.waitForTimeout(challengeExpiry + 5000);

// Start polling
await page.getByRole('button', { name: 'Start polling' }).click();
await expect(page.getByText('Polling...')).toBeVisible();

// Check for expired status
await expect(page.getByText('Error: expired')).toBeVisible();

// Check poll count
expect(numPollRequests === 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Incorrect assertion - test will always pass.

Same issue as Line 83: expect(numPollRequests === 0) evaluates the boolean expression first and passes it to expect(), which will always succeed.

🐛 Proposed fix
     // Check poll count
-    expect(numPollRequests === 0);
+    expect(numPollRequests).toBe(0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(numPollRequests === 0);
expect(numPollRequests).toBe(0);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/davinci-suites/src/polling.test.ts` at line 122, The assertion currently
passes a boolean expression into expect (expect(numPollRequests === 0)) which
evaluates before expect and thus the test always passes; update the assertion to
pass the value and use a matcher—replace the incorrect call with
expect(numPollRequests).toBe(0) (or another appropriate matcher) so the test
actually asserts that numPollRequests equals zero.

});
});

test.describe('Continue Polling', () => {
test('should succeed on QR code scan simulation', async ({ page }) => {
const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0';
const davinciPolicy = '27aacf0efcc480dfcd00b04be8023cdc';
const { navigate } = asyncEvents(page);
await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`);

await expect(page.url()).toBe(
`http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`,
);

await expect(page.getByRole('heading', { name: 'Select Continue Polling Test' })).toBeVisible();
await page.getByRole('button', { name: 'Success' }).click();
await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible();

// Start polling
const numberCounterSuccess = 2;
for (let i = 0; i < numberCounterSuccess; i++) {
await page.getByRole('button', { name: 'Start polling' }).click();
await expect(page.getByText('Polling...')).toBeVisible();
await expect(page.getByRole('button', { name: 'Start polling' })).toBeDisabled();
}

// Check for success
await expect(page.getByText('Message: Done')).toBeVisible();
});

test('should timeout when retries are exhausted', async ({ page }) => {
const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0';
const davinciPolicy = '27aacf0efcc480dfcd00b04be8023cdc';
const { navigate } = asyncEvents(page);
await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`);

await expect(page.url()).toBe(
`http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`,
);

await expect(page.getByRole('heading', { name: 'Select Continue Polling Test' })).toBeVisible();
await page.getByRole('button', { name: 'Timeout' }).click();
await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible();

// Start polling
const maxRetries = 3;
for (let i = 0; i < maxRetries + 1; i++) {
await page.getByRole('button', { name: 'Start polling' }).click();
await expect(page.getByText('Polling...')).toBeVisible();
await expect(page.getByRole('button', { name: 'Start polling' })).toBeDisabled();
}

// Check for timeout
await expect(page.getByText('Error: timedOut')).toBeVisible();
});
});
4 changes: 2 additions & 2 deletions e2e/davinci-suites/src/protect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ test('Test Protect collector with Custom HTML component', async ({ page }) => {

await expect(page.getByText('JS Protect - Custom HTML Form')).toBeVisible();

const requests = [];
const requests: string[] = [];
page.on('request', (request) => {
const method = request.method();
const requestUrl = request.url();
Expand Down Expand Up @@ -54,7 +54,7 @@ test('Test Protect collector with P1 Forms component', async ({ page }) => {

await expect(page.getByText('Example - Sign On')).toBeVisible();

const requests = [];
const requests: string[] = [];
page.on('request', (request) => {
const method = request.method();
const requestUrl = request.url();
Expand Down
7 changes: 3 additions & 4 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ pre-commit:
nx-check:
run: pnpm nx affected -t typecheck lint build api-report --tui=false
stage_fixed: true
format:
run: pnpm nx format:write
stage_fixed: true
interface-mapping:
glob: >-
{tools/interface-mapping-validator/**/*.ts,
Expand All @@ -24,7 +21,9 @@ pre-commit:
echo "Interface mapping is out of sync." &&
echo "Run: pnpm mapping:generate" &&
exit 1)

format:
run: pnpm nx format:write
stage_fixed: true
commit-msg:
commands:
commitlint:
Expand Down
Loading