Skip to content
Merged
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@
All notable changes to `@ngbracket/a11y-devtools` are documented here.
This project adheres to [Semantic Versioning](https://semver.org/).

## 0.18.3

### Fixed

- Report mode now scans pages served with a strict Content-Security-Policy or
Trusted Types. The scan runs as an injected script, which a policy such as
`script-src 'self'` or `require-trusted-types-for 'script'` blocked, so the
page was reported as an error with no findings. The scanning browser now
ignores the page's CSP, whether it comes from a header or a `<meta>` tag.
Your app's policy is unchanged. Because the scan runs without the policy,
anything the policy would block for real users (a web font, an inline style)
loads during the scan, so a problem caused by the CSP itself isn't reported.

## 0.18.2

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngbracket/a11y-devtools",
"version": "0.18.2",
"version": "0.18.3",
"description": "Finds accessibility problems in Angular apps and names the component behind each one. A dev overlay, or headless reports for CI.",
"license": "MIT",
"author": "Duncan Faulkner",
Expand Down
9 changes: 8 additions & 1 deletion src/report/headless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,14 @@ async function scanPass(
config: PassConfig,
): Promise<PageReport[]> {
const { baseUrl, routes, labels, waitMs, axeOptions, inPageScript, frameworkPrefixes } = config;
const context = await browser.newContext({ viewport: { width: 1280, height: 900 }, colorScheme });
// bypassCSP: the scan runs as an injected inline script (plus a style tag that
// hides the overlay), which a strict Content-Security-Policy or Trusted Types
// would block. It only lifts the policy in this scanning browser.
const context = await browser.newContext({
viewport: { width: 1280, height: 900 },
colorScheme,
bypassCSP: true,
});
const pages: PageReport[] = [];
try {
const page = await context.newPage();
Expand Down
40 changes: 38 additions & 2 deletions src/testing/e2e-report.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ const PAGES: Record<string, string> = {
<button type="button"><svg width="16" height="16" aria-hidden="true"></svg></button>
<a href="#help" tabindex="2">Help</a>
</main>`,
// Served with a strict Content-Security-Policy and Trusted Types (see the
// server below), which block the injected scan unless the browser bypasses CSP.
'/csp': `
<main>
<h1>Strict CSP</h1>
<img src="data:image/gif;base64,R0lGODlhAQABAAAAACw=">
</main>`,
// The same policy set by a <meta http-equiv> tag instead (the server adds it
// to <head>), which is how many Angular apps ship a CSP.
'/csp-meta': `
<main>
<h1>Strict CSP in a meta tag</h1>
<img src="data:image/gif;base64,R0lGODlhAQABAAAAACw=">
</main>`,
// Healthy: native controls in order, plus an iframe (Tab inside a frame shows
// up as repeated focus on the <iframe>, which must not read as a trap).
'/clean': `
Expand Down Expand Up @@ -198,6 +212,10 @@ const PAGES: Record<string, string> = {
<main><h1>Sidebar</h1></main>`,
};

/** Blocks inline scripts and styles and requires Trusted Types. */
const STRICT_CSP =
"default-src 'self'; img-src data:; script-src 'self'; style-src 'self'; require-trusted-types-for 'script'";

function html(body: string): string {
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Fixture</title></head><body>${body}</body></html>`;
}
Expand All @@ -210,8 +228,18 @@ describe.skipIf(!ready)('report-mode in a real browser (E2E)', () => {
beforeAll(async () => {
server = createServer((req, res) => {
const body = PAGES[req.url ?? ''];
res.writeHead(body ? 200 : 404, { 'content-type': 'text/html; charset=utf-8' });
res.end(body ? html(body) : 'not found');
res.writeHead(body ? 200 : 404, {
'content-type': 'text/html; charset=utf-8',
...(req.url === '/csp' && {
'content-security-policy': STRICT_CSP,
}),
});
const page = body ? html(body) : 'not found';
res.end(
req.url === '/csp-meta'
? page.replace('<head>', `<head><meta http-equiv="Content-Security-Policy" content="${STRICT_CSP}">`)
: page,
);
});
await new Promise<void>((done) => server.listen(0, '127.0.0.1', done));
baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
Expand Down Expand Up @@ -260,6 +288,14 @@ describe.skipIf(!ready)('report-mode in a real browser (E2E)', () => {
expect(report.pages.map((p) => p.label)).toEqual(Object.keys(PAGES));
});

it('scans a page with a strict Content-Security-Policy and Trusted Types', () => {
expect(ids('/csp')).toContain('image-alt');
});

it('scans a page whose strict policy is set in a meta tag', () => {
expect(ids('/csp-meta')).toContain('image-alt');
});

it('checks contrast below the fold even when the app runs the in-app overlay', () => {
const below = page('/overlay-below-fold').findings.filter((f) => f.html.includes('below-fold'));
expect(below.map((f) => f.id)).toEqual(['color-contrast']);
Expand Down
Loading