Skip to content
Closed
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
3 changes: 3 additions & 0 deletions browser-tests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
test-results/
playwright-report/
16 changes: 16 additions & 0 deletions browser-tests/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "browser-tests",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"@playwright/test": "^1.57.0"
}
}
29 changes: 29 additions & 0 deletions browser-tests/playwright.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// @ts-check
const { defineConfig, devices } = require('@playwright/test');

module.exports = defineConfig({
testDir: '.',
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: 0,
workers: 1,
reporter: [['list'], ['html', { open: 'never' }]],
use: {
trace: 'on-first-retry',
},

projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
246 changes: 246 additions & 0 deletions browser-tests/trusted-types.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
// @ts-check
const { test, expect } = require('@playwright/test');

// Test server that serves pages with specific CSP headers
const http = require('http');

let server;
let serverPort;

test.beforeAll(async () => {
server = http.createServer((req, res) => {
const url = new URL(req.url, `http://localhost`);
const csp = url.searchParams.get('csp') || '';

res.setHeader('Content-Type', 'text/html');
if (csp) {
res.setHeader('Content-Security-Policy', csp);
}

res.end(`<!DOCTYPE html>
<html>
<head><title>Trusted Types Test</title></head>
<body>
<div id="result"></div>
<script>
window.testResults = {};

// Test if trustedTypes is supported
window.testResults.trustedTypesSupported = typeof trustedTypes !== 'undefined';

// Try to create policies and record results
window.tryCreatePolicy = function(name) {
try {
const policy = trustedTypes.createPolicy(name, {
createHTML: (s) => s,
createScript: (s) => s,
createScriptURL: (s) => s,
});
return { success: true, policyName: policy.name };
} catch (e) {
return { success: false, error: e.message, errorType: e.name };
}
};
</script>
</body>
</html>`);
});

await new Promise((resolve) => {
server.listen(0, () => {
serverPort = server.address().port;
resolve();
});
});
});

test.afterAll(async () => {
if (server) {
server.close();
}
});

test.describe('Trusted Types keyword case sensitivity', () => {

test('lowercase allow-duplicates keyword', async ({ page }) => {
const csp = "trusted-types myPolicy 'allow-duplicates'";
await page.goto(`http://localhost:${serverPort}?csp=${encodeURIComponent(csp)}`);

// Create the same policy twice - should succeed with allow-duplicates
const result1 = await page.evaluate(() => window.tryCreatePolicy('myPolicy'));
const result2 = await page.evaluate(() => window.tryCreatePolicy('myPolicy'));

console.log('lowercase allow-duplicates:', { result1, result2 });

expect(result1.success).toBe(true);
expect(result2.success).toBe(true); // Should succeed due to allow-duplicates
});

test('UPPERCASE ALLOW-DUPLICATES keyword', async ({ page }) => {
const csp = "trusted-types myPolicy 'ALLOW-DUPLICATES'";
await page.goto(`http://localhost:${serverPort}?csp=${encodeURIComponent(csp)}`);

// Create the same policy twice
const result1 = await page.evaluate(() => window.tryCreatePolicy('myPolicy'));
const result2 = await page.evaluate(() => window.tryCreatePolicy('myPolicy'));

console.log('UPPERCASE ALLOW-DUPLICATES:', { result1, result2 });

// Record whether uppercase worked
test.info().annotations.push({
type: 'browser-behavior',
description: `UPPERCASE 'ALLOW-DUPLICATES': first=${result1.success}, second=${result2.success}`
});
});

test('MixedCase Allow-Duplicates keyword', async ({ page }) => {
const csp = "trusted-types myPolicy 'Allow-Duplicates'";
await page.goto(`http://localhost:${serverPort}?csp=${encodeURIComponent(csp)}`);

const result1 = await page.evaluate(() => window.tryCreatePolicy('myPolicy'));
const result2 = await page.evaluate(() => window.tryCreatePolicy('myPolicy'));

console.log('MixedCase Allow-Duplicates:', { result1, result2 });

test.info().annotations.push({
type: 'browser-behavior',
description: `MixedCase 'Allow-Duplicates': first=${result1.success}, second=${result2.success}`
});
});

test('lowercase none keyword', async ({ page }) => {
const csp = "trusted-types 'none'";
await page.goto(`http://localhost:${serverPort}?csp=${encodeURIComponent(csp)}`);

const result = await page.evaluate(() => window.tryCreatePolicy('anyPolicy'));

console.log("lowercase 'none':", result);

// 'none' should block all policy creation
expect(result.success).toBe(false);
});

test('UPPERCASE NONE keyword', async ({ page }) => {
const csp = "trusted-types 'NONE'";
await page.goto(`http://localhost:${serverPort}?csp=${encodeURIComponent(csp)}`);

const result = await page.evaluate(() => window.tryCreatePolicy('anyPolicy'));

console.log("UPPERCASE 'NONE':", result);

test.info().annotations.push({
type: 'browser-behavior',
description: `UPPERCASE 'NONE' blocks policy creation: ${!result.success}`
});
});
});

test.describe('Trusted Types policy name case sensitivity', () => {

test('exact case policy name match', async ({ page }) => {
const csp = "trusted-types myPolicy";
await page.goto(`http://localhost:${serverPort}?csp=${encodeURIComponent(csp)}`);

const result = await page.evaluate(() => window.tryCreatePolicy('myPolicy'));

console.log('exact case myPolicy:', result);

expect(result.success).toBe(true);
});

test('different case policy name - lowercase csp, uppercase create', async ({ page }) => {
const csp = "trusted-types mypolicy"; // lowercase in CSP
await page.goto(`http://localhost:${serverPort}?csp=${encodeURIComponent(csp)}`);

const result = await page.evaluate(() => window.tryCreatePolicy('MYPOLICY')); // UPPERCASE in JS

console.log('lowercase CSP, UPPERCASE create:', result);

test.info().annotations.push({
type: 'browser-behavior',
description: `CSP 'mypolicy' allows creating 'MYPOLICY': ${result.success}`
});
});

test('different case policy name - uppercase csp, lowercase create', async ({ page }) => {
const csp = "trusted-types MYPOLICY"; // UPPERCASE in CSP
await page.goto(`http://localhost:${serverPort}?csp=${encodeURIComponent(csp)}`);

const result = await page.evaluate(() => window.tryCreatePolicy('mypolicy')); // lowercase in JS

console.log('UPPERCASE CSP, lowercase create:', result);

test.info().annotations.push({
type: 'browser-behavior',
description: `CSP 'MYPOLICY' allows creating 'mypolicy': ${result.success}`
});
});

test('mixed case variations', async ({ page }) => {
const csp = "trusted-types MyPolicy";
await page.goto(`http://localhost:${serverPort}?csp=${encodeURIComponent(csp)}`);

const exactMatch = await page.evaluate(() => window.tryCreatePolicy('MyPolicy'));

// Reset page for each test to get fresh trustedTypes state
await page.goto(`http://localhost:${serverPort}?csp=${encodeURIComponent(csp)}`);
const lowerCase = await page.evaluate(() => window.tryCreatePolicy('mypolicy'));

await page.goto(`http://localhost:${serverPort}?csp=${encodeURIComponent(csp)}`);
const upperCase = await page.evaluate(() => window.tryCreatePolicy('MYPOLICY'));

console.log('mixed case variations:', { exactMatch, lowerCase, upperCase });

test.info().annotations.push({
type: 'browser-behavior',
description: `CSP 'MyPolicy': exact=${exactMatch.success}, lower=${lowerCase.success}, upper=${upperCase.success}`
});
});
});

test.describe('Comprehensive case sensitivity summary', () => {
test('full case sensitivity test', async ({ page }) => {
const results = {};

// Test 1: Does 'ALLOW-DUPLICATES' work like 'allow-duplicates'?
let csp = "trusted-types testPolicy 'ALLOW-DUPLICATES'";
await page.goto(`http://localhost:${serverPort}?csp=${encodeURIComponent(csp)}`);
const dup1 = await page.evaluate(() => window.tryCreatePolicy('testPolicy'));
const dup2 = await page.evaluate(() => window.tryCreatePolicy('testPolicy'));
results.uppercaseAllowDuplicatesWorks = dup1.success && dup2.success;

// Test 2: Does 'NONE' work like 'none'?
csp = "trusted-types 'NONE'";
await page.goto(`http://localhost:${serverPort}?csp=${encodeURIComponent(csp)}`);
const noneResult = await page.evaluate(() => window.tryCreatePolicy('anyPolicy'));
results.uppercaseNoneBlocks = !noneResult.success;

// Test 3: Are policy names case-sensitive?
csp = "trusted-types CaseSensitivePolicy";
await page.goto(`http://localhost:${serverPort}?csp=${encodeURIComponent(csp)}`);
const exactCase = await page.evaluate(() => window.tryCreatePolicy('CaseSensitivePolicy'));
results.exactCaseWorks = exactCase.success;

await page.goto(`http://localhost:${serverPort}?csp=${encodeURIComponent(csp)}`);
const wrongCase = await page.evaluate(() => window.tryCreatePolicy('casesensitivepolicy'));
results.wrongCaseWorks = wrongCase.success;

results.policyNamesAreCaseSensitive = results.exactCaseWorks && !results.wrongCaseWorks;
results.policyNamesAreCaseInsensitive = results.exactCaseWorks && results.wrongCaseWorks;

console.log('\n========================================');
console.log('BROWSER BEHAVIOR SUMMARY:');
console.log('========================================');
console.log(`Keywords case-insensitive (ALLOW-DUPLICATES works): ${results.uppercaseAllowDuplicatesWorks}`);
console.log(`Keywords case-insensitive (NONE works): ${results.uppercaseNoneBlocks}`);
console.log(`Policy names are CASE-SENSITIVE: ${results.policyNamesAreCaseSensitive}`);
console.log(`Policy names are CASE-INSENSITIVE: ${results.policyNamesAreCaseInsensitive}`);
console.log('========================================\n');

// Store results for the test report
test.info().annotations.push({
type: 'summary',
description: JSON.stringify(results, null, 2)
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.shapesecurity.salvation2.Directives;

import com.shapesecurity.salvation2.Directive;
import com.shapesecurity.salvation2.Policy;

import java.util.List;
import java.util.Locale;

public class RequireTrustedTypesForDirective extends Directive {
// https://w3c.github.io/trusted-types/dist/spec/#require-trusted-types-for-csp-directive
// Currently only 'script' is defined
private static final String SCRIPT = "'script'";

private boolean script = false;

public RequireTrustedTypesForDirective(List<String> values, DirectiveErrorConsumer errors) {
super(values);

if (values.isEmpty()) {
errors.add(Policy.Severity.Error, "The require-trusted-types-for directive requires a value", -1);
return;
}

int index = 0;
for (String token : values) {
// ABNF strings are case-insensitive
String lowcaseToken = token.toLowerCase(Locale.ENGLISH);
switch (lowcaseToken) {
case "'script'":
if (!this.script) {
this.script = true;
} else {
errors.add(Policy.Severity.Warning, "Duplicate keyword 'script'", index);
}
break;
default:
if (token.startsWith("'") && token.endsWith("'")) {
errors.add(Policy.Severity.Error, "Unrecognized require-trusted-types-for keyword " + token, index);
} else {
errors.add(Policy.Severity.Error, "Unrecognized require-trusted-types-for value " + token + " - keywords must be wrapped in single quotes", index);
}
}
++index;
}
}


public boolean script() {
return this.script;
}

public void setScript(boolean script) {
if (this.script == script) {
return;
}
if (script) {
this.addValue(SCRIPT);
} else {
this.removeValueIgnoreCase(SCRIPT);
}
this.script = script;
}
}
Loading
Loading