Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new_audit: ensure proper origin isolation with COOP #16275

Merged
merged 24 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2df023e
Add files to gitignore.
sebastian9er Nov 15, 2024
f95d133
Merge remote-tracking branch 'upstream/main'
sebastian9er Nov 15, 2024
e52e1b8
Merge branch 'GoogleChrome:main' into main
sebastian9er Nov 27, 2024
3861727
Merge branch 'main' of https://github.com/sebastian9er/lighthouse
sebastian9er Dec 4, 2024
8717e3c
Commit gitignore
sebastian9er Dec 4, 2024
48c45a6
Add origin isolation / coop header audit.
sebastian9er Dec 4, 2024
ea38eb7
Adding missing files for the origin isolation audit.
sebastian9er Dec 4, 2024
c28c7a4
Fix minor but annoying error in the origin isolation audit and update…
sebastian9er Dec 4, 2024
d46b698
Reverting files requested in PR https://github.com/GoogleChrome/light…
sebastian9er Dec 5, 2024
d57f1bc
Revert gitignore file.
sebastian9er Dec 5, 2024
22088cd
Fix return of constructResults.
sebastian9er Dec 5, 2024
580517f
Update cli/test/smokehouse/frontends/smokehouse-bin.js
sebastian9er Dec 9, 2024
e539993
Update core/audits/origin-isolation.js
sebastian9er Dec 9, 2024
33e6c66
Fix description to clarify what COOP stands for.
sebastian9er Dec 9, 2024
3948652
smokehouse
adamraine Dec 9, 2024
21b1440
tests
adamraine Dec 10, 2024
0fc3934
sample
adamraine Dec 10, 2024
24b230c
Addign the noopener-allow-popups keyword to the COOP header lookup.
sebastian9er Dec 11, 2024
7f1b7e0
Update core/audits/origin-isolation.js
adamraine Dec 11, 2024
341e05d
Update core/audits/origin-isolation.js
adamraine Dec 11, 2024
0acd035
Update core/audits/origin-isolation.js
adamraine Dec 11, 2024
bcddfc7
lint
adamraine Dec 11, 2024
b323a13
unhide
adamraine Dec 12, 2024
468f13d
sample
adamraine Dec 12, 2024
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: 4 additions & 0 deletions cli/test/smokehouse/core-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import metricsTrickyTti from './test-definitions/metrics-tricky-tti.js';
import metricsTrickyTtiLateFcp from './test-definitions/metrics-tricky-tti-late-fcp.js';
import oopifRequests from './test-definitions/oopif-requests.js';
import oopifScripts from './test-definitions/oopif-scripts.js';
import originIsolationCoopHeaderMissing from './test-definitions/origin-isolation-coop-header-missing.js';
import originIsolationCoopPresent from './test-definitions/origin-isolation-coop-present.js';
import perfDebug from './test-definitions/perf-debug.js';
import perfDiagnosticsAnimations from './test-definitions/perf-diagnostics-animations.js';
import perfDiagnosticsThirdParty from './test-definitions/perf-diagnostics-third-party.js';
Expand Down Expand Up @@ -97,6 +99,8 @@ const smokeTests = [
metricsTrickyTtiLateFcp,
oopifRequests,
oopifScripts,
originIsolationCoopHeaderMissing,
originIsolationCoopPresent,
perfDebug,
perfDiagnosticsAnimations,
perfDiagnosticsThirdParty,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* @type {Smokehouse.ExpectedRunnerResult}
* Expected Lighthouse results for a site with a missing COOP header.
*/
const expectations = {
lhr: {
requestedUrl: 'https://example.com/',
finalDisplayedUrl: 'https://example.com/',
audits: {
'origin-isolation': {
score: 1,
details: {
items: [
{
description: 'No COOP header found',
severity: 'High',
},
],
},
},
},
},
};

export default {
id: 'origin-isolation-coop-header-missing',
expectations,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* @type {Smokehouse.ExpectedRunnerResult}
* Expected Lighthouse results for a site with a configured COOP header.
*/
const expectations = {
lhr: {
requestedUrl: 'https://csp.withgoogle.com/docs/index.html',
finalDisplayedUrl: 'https://csp.withgoogle.com/docs/index.html',
audits: {
'origin-isolation': {
score: null,
},
},
},
};

export default {
id: 'origin-isolation-coop-present',
expectations,
};
154 changes: 154 additions & 0 deletions core/audits/origin-isolation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {Audit} from './audit.js';
import {MainResource} from '../computed/main-resource.js';
import * as i18n from '../lib/i18n/i18n.js';

const UIStrings = {
/** Title of a Lighthouse audit that evaluates the security of a page's COOP header for origin isolation. "COOP" stands for "Cross-Origin-Opener-Policy". */
adamraine marked this conversation as resolved.
Show resolved Hide resolved
title: 'Ensure proper origin isolation with COOP',
/** Description of a Lighthouse audit that evaluates the security of a page's COOP header for origin isolation. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. "COOP" stands for "Cross-Origin-Opener-Policy". */
adamraine marked this conversation as resolved.
Show resolved Hide resolved
description: 'Deployment of the Cross-Origin-Opener-Policy (COOP) header allows isolation of the top-level document to not share a browsing context group with cross-origin documents. ' +
'[Learn what the COOP header is and how it should be deployed.](https://web.dev/articles/why-coop-coep#coop)',
/** Summary text for the results of a Lighthouse audit that evaluates the COOP header for origin isolation. This is displayed if no COOP header is deployed. "COOP" stands for "Cross-Origin-Opener-Policy". */
adamraine marked this conversation as resolved.
Show resolved Hide resolved
noCoop: 'No COOP header found',
/** Table item value calling out the presence of a syntax error. */
invalidSyntax: 'Invalid syntax',
/** Label for a column in a data table; entries will be a directive of the COOP header. "COOP" stands for "Cross-Origin-Opener-Policy". */
columnDirective: 'Directive',
/** Label for a column in a data table; entries will be the severity of an issue with the COOP header. "COOP" stands for "Cross-Origin-Opener-Policy". */
columnSeverity: 'Severity',
};

const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);

class OriginIsolation extends Audit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'origin-isolation',
scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
title: str_(UIStrings.title),
description: str_(UIStrings.description),
requiredArtifacts: ['devtoolsLogs', 'URL'],
supportedModes: ['navigation'],
};
}


/**
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
* @return {Promise<string[]>}
*/
static async getRawCoop(artifacts, context) {
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
const mainResource =
await MainResource.request({devtoolsLog, URL: artifacts.URL}, context);

let coopHeaders =
mainResource.responseHeaders
.filter(h => {
return h.name.toLowerCase() === 'cross-origin-opener-policy';
})
.flatMap(h => h.value);

// Sanitize the header value.
coopHeaders = coopHeaders.map(v => v.toLowerCase().replace(/\s/g, ''));

return coopHeaders;
}

/**
* @param {string | undefined} coopDirective
* @param {LH.IcuMessage | string} findingDescription
* @param {LH.IcuMessage=} severity
* @return {LH.Audit.Details.TableItem}
*/
static findingToTableItem(coopDirective, findingDescription, severity) {
return {
directive: coopDirective,
description: findingDescription,
severity,
};
}

/**
* @param {string[]} coopHeaders
* @return {{score: number, results: LH.Audit.Details.TableItem[]}}
*/
static constructResults(coopHeaders) {
const rawCoop = [...coopHeaders];
const allowedDirectives =
['unsafe-none', 'same-origin-allow-popups', 'same-origin'];
sebastian9er marked this conversation as resolved.
Show resolved Hide resolved
const violations = [];
const syntax = [];

if (!rawCoop.length) {
violations.push({
severity: str_(i18n.UIStrings.itemSeverityHigh),
description: str_(UIStrings.noCoop),
directive: undefined,
});
}

for (const actualDirective of coopHeaders) {
// If there is a directive that's not an official COOP directive.
if (!allowedDirectives.includes(actualDirective)) {
syntax.push({
severity: str_(i18n.UIStrings.itemSeverityLow),
description: str_(UIStrings.invalidSyntax),
directive: actualDirective,
});
}
}

const results = [
...violations.map(
f => this.findingToTableItem(
f.directive, f.description,
str_(i18n.UIStrings.itemSeverityHigh))),
...syntax.map(
f => this.findingToTableItem(
f.directive, f.description,
str_(i18n.UIStrings.itemSeverityLow))),
];

return {score: violations.length || syntax.length ? 0 : 1, results};
}

/**
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
* @return {Promise<LH.Audit.Product>}
*/
static async audit(artifacts, context) {
const coopHeaders = await this.getRawCoop(artifacts, context);
const {score, results} = this.constructResults(coopHeaders);

/** @type {LH.Audit.Details.Table['headings']} */
const headings = [
/* eslint-disable max-len */
{key: 'description', valueType: 'text', subItemsHeading: {key: 'description'}, label: str_(i18n.UIStrings.columnDescription)},
{key: 'directive', valueType: 'code', subItemsHeading: {key: 'directive'}, label: str_(UIStrings.columnDirective)},
{key: 'severity', valueType: 'text', subItemsHeading: {key: 'severity'}, label: str_(UIStrings.columnSeverity)},
/* eslint-enable max-len */
];
const details = Audit.makeTableDetails(headings, results);

return {
score,
notApplicable: !results.length,
details,
};
}
}

export default OriginIsolation;
export {UIStrings};
2 changes: 2 additions & 0 deletions core/config/default-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ const defaultConfig = {
'prioritize-lcp-image',
'csp-xss',
'has-hsts',
'origin-isolation',
'script-treemap-data',
'accessibility/accesskeys',
'accessibility/aria-allowed-attr',
Expand Down Expand Up @@ -543,6 +544,7 @@ const defaultConfig = {
{id: 'notification-on-start', weight: 1, group: 'best-practices-trust-safety'},
{id: 'csp-xss', weight: 0, group: 'best-practices-trust-safety'},
{id: 'has-hsts', weight: 0, group: 'best-practices-trust-safety'},
{id: 'origin-isolation', weight: 0, group: 'hidden'},
// User Experience
{id: 'paste-preventing-inputs', weight: 3, group: 'best-practices-ux'},
{id: 'image-aspect-ratio', weight: 1, group: 'best-practices-ux'},
Expand Down
2 changes: 2 additions & 0 deletions core/scripts/i18n/collect-strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,7 @@ function checkKnownFixedCollisions(strings) {
'Consider uploading your GIF to a service which will make it available to embed as an HTML5 video.',
'Directive',
'Directive',
'Directive',
'Document contains a $MARKDOWN_SNIPPET_0$ that triggers $MARKDOWN_SNIPPET_1$',
'Document contains a $MARKDOWN_SNIPPET_0$ that triggers $MARKDOWN_SNIPPET_1$',
'Document has a valid $MARKDOWN_SNIPPET_0$',
Expand All @@ -751,6 +752,7 @@ function checkKnownFixedCollisions(strings) {
'Potential Savings',
'Severity',
'Severity',
'Severity',
'The page was evicted from the cache to allow another page to be cached.',
'The page was evicted from the cache to allow another page to be cached.',
'Use $MARKDOWN_SNIPPET_0$ to detect unused JavaScript code. $LINK_START_0$Learn more$LINK_END_0$',
Expand Down
Loading
Loading