-
Notifications
You must be signed in to change notification settings - Fork 9.4k
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 clickjacking mitigation through XFO or CSP #16290
base: main
Are you sure you want to change the base?
Changes from all commits
2df023e
f95d133
e52e1b8
3861727
8717e3c
009af4e
069260b
8b123ab
aac2789
cd5761f
6c11434
de85518
7f10f7f
ba82107
feabb42
6d5b543
9eaca8f
268229b
5d86c36
1717db0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/** | ||
* @license | ||
* Copyright 2024 Google LLC | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
/** | ||
* @type {Smokehouse.ExpectedRunnerResult} | ||
* Expected Lighthouse results for a site with missing Clickjacking mitigations | ||
* (through the X-Frame-Options or Content-Security-Policy headers). | ||
*/ | ||
const expectations = { | ||
lhr: { | ||
requestedUrl: 'https://example.com/', | ||
finalDisplayedUrl: 'https://example.com/', | ||
audits: { | ||
'clickjacking-mitigation': { | ||
score: 0, | ||
details: { | ||
items: [ | ||
{ | ||
description: 'No Clickjacking mitigation found.', | ||
severity: 'High', | ||
}, | ||
], | ||
}, | ||
}, | ||
}, | ||
}, | ||
}; | ||
|
||
export default { | ||
id: 'clickjacking-missing-headers', | ||
expectations, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/** | ||
* @license | ||
* Copyright 2024 Google LLC | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
/** | ||
* @type {Smokehouse.ExpectedRunnerResult} | ||
* Expected Lighthouse results for a site with present Clickjacking mitigations | ||
* (through the X-Frame-Options or Content-Security-Policy headers). | ||
*/ | ||
const expectations = { | ||
lhr: { | ||
requestedUrl: 'https://developer.mozilla.org/en-US/', | ||
finalDisplayedUrl: 'https://developer.mozilla.org/en-US/', | ||
audits: { | ||
'clickjacking-mitigation': { | ||
score: null, | ||
}, | ||
}, | ||
}, | ||
}; | ||
|
||
export default { | ||
id: 'clickjacking-mitigation-headers-present', | ||
expectations, | ||
}; |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,159 @@ | ||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||
* @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 whether the set CSP or XFO header is mitigating Clickjacking attacks. "XFO" stands for "X-Frame-Options". "CSP" stands for "Content-Security-Policy". */ | ||||||||||||||||||||||||||||||||||
title: 'Ensure Clickjacking mitigation through XFO or CSP.', | ||||||||||||||||||||||||||||||||||
/** Description of a Lighthouse audit that evaluates whether the set CSP or XFO header is mitigating Clickjacking attacks. 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. "XFO" stands for "X-Frame-Options". "CSP" stands for "Content-Security-Policy". */ | ||||||||||||||||||||||||||||||||||
description: 'Deployment of either the X-Frame-Options or Content-Security-Policy (with the frame-ancestors directive) header will prevent Clickjacking attacks. While the XFO header is simpler to deploy, the CSP header is more flexible. [Learn more about mitigating Clickjacking with XFO and CSP](https://developer.chrome.com/docs/lighthouse/best-practices/clickjacking-mitigation).', | ||||||||||||||||||||||||||||||||||
/** Summary text for the results of a Lighthouse audit that evaluates whether the set CSP or XFO header is mitigating Clickjacking attacks. This is displayed if there is neither a CSP nor XFO header deployed. "XFO" stands for "X-Frame-Options". "CSP" stands for "Content-Security-Policy". */ | ||||||||||||||||||||||||||||||||||
noClickjackingMitigation: 'No Clickjacking mitigation found.', | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||
/** Label for a column in a data table; entries will be a directive of the XFO or CSP header. "XFO" stands for "X-Frame-Options". "CSP" stands for "Content-Security-Policy". */ | ||||||||||||||||||||||||||||||||||
columnDirective: 'Directive', | ||||||||||||||||||||||||||||||||||
/** Label for a column in a data table; entries will be the severity of an issue with the XFO or CSP header. "XFO" stands for "X-Frame-Options". "CSP" stands for "Content-Security-Policy". */ | ||||||||||||||||||||||||||||||||||
columnSeverity: 'Severity', | ||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
class ClickjackingMitigation extends Audit { | ||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||
* @return {LH.Audit.Meta} | ||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||
static get meta() { | ||||||||||||||||||||||||||||||||||
return { | ||||||||||||||||||||||||||||||||||
id: 'clickjacking-mitigation', | ||||||||||||||||||||||||||||||||||
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<{cspHeaders: string[], xfoHeaders: string[]}>} | ||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||
static async getRawCspsAndXfo(artifacts, context) { | ||||||||||||||||||||||||||||||||||
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS]; | ||||||||||||||||||||||||||||||||||
const mainResource = | ||||||||||||||||||||||||||||||||||
await MainResource.request({devtoolsLog, URL: artifacts.URL}, context); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
const cspHeaders = mainResource.responseHeaders | ||||||||||||||||||||||||||||||||||
.filter(h => { | ||||||||||||||||||||||||||||||||||
return h.name.toLowerCase() === 'content-security-policy'; | ||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||
.flatMap(h => h.value.split(',')) | ||||||||||||||||||||||||||||||||||
.filter(rawCsp => rawCsp.replace(/\s/g, '')); | ||||||||||||||||||||||||||||||||||
let xfoHeaders = mainResource.responseHeaders | ||||||||||||||||||||||||||||||||||
.filter(h => { | ||||||||||||||||||||||||||||||||||
return h.name.toLowerCase() === 'x-frame-options'; | ||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||
.flatMap(h => h.value); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// Sanitize the XFO header value. | ||||||||||||||||||||||||||||||||||
xfoHeaders = xfoHeaders.map(v => v.toLowerCase().replace(/\s/g, '')); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
return {cspHeaders, xfoHeaders}; | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||
* @param {string | undefined} directive | ||||||||||||||||||||||||||||||||||
* @param {LH.IcuMessage | string} findingDescription | ||||||||||||||||||||||||||||||||||
* @param {LH.IcuMessage=} severity | ||||||||||||||||||||||||||||||||||
* @return {LH.Audit.Details.TableItem} | ||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||
static findingToTableItem(directive, findingDescription, severity) { | ||||||||||||||||||||||||||||||||||
return { | ||||||||||||||||||||||||||||||||||
directive: directive, | ||||||||||||||||||||||||||||||||||
description: findingDescription, | ||||||||||||||||||||||||||||||||||
severity, | ||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||
* @param {string[]} cspHeaders | ||||||||||||||||||||||||||||||||||
* @param {string[]} xfoHeaders | ||||||||||||||||||||||||||||||||||
* @return {{score: number, results: LH.Audit.Details.TableItem[]}} | ||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||
static constructResults(cspHeaders, xfoHeaders) { | ||||||||||||||||||||||||||||||||||
const rawXfo = [...xfoHeaders]; | ||||||||||||||||||||||||||||||||||
const allowedDirectives = ['deny', 'sameorigin']; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// If there is none of the two headers, return early. | ||||||||||||||||||||||||||||||||||
if (!rawXfo.length && !cspHeaders.length) { | ||||||||||||||||||||||||||||||||||
return { | ||||||||||||||||||||||||||||||||||
score: 0, | ||||||||||||||||||||||||||||||||||
results: [{ | ||||||||||||||||||||||||||||||||||
severity: str_(i18n.UIStrings.itemSeverityHigh), | ||||||||||||||||||||||||||||||||||
description: str_(UIStrings.noClickjackingMitigation), | ||||||||||||||||||||||||||||||||||
directive: undefined, | ||||||||||||||||||||||||||||||||||
}], | ||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
Comment on lines
+92
to
+102
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This is redundant, if the header lists are empty we will just skip to the bottom anyway. |
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// Check for frame-ancestors in CSP. | ||||||||||||||||||||||||||||||||||
if (cspHeaders.length) { | ||||||||||||||||||||||||||||||||||
for (const cspDirective of cspHeaders) { | ||||||||||||||||||||||||||||||||||
if (cspDirective.includes('frame-ancestors')) { | ||||||||||||||||||||||||||||||||||
// Pass the audit if frame-ancestors is present. | ||||||||||||||||||||||||||||||||||
return {score: 1, results: []}; | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
Comment on lines
+104
to
+112
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
for (const actualDirective of xfoHeaders) { | ||||||||||||||||||||||||||||||||||
if (allowedDirectives.includes(actualDirective)) { | ||||||||||||||||||||||||||||||||||
// DENY or SAMEORIGIN are present. | ||||||||||||||||||||||||||||||||||
return {score: 1, results: []}; | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
return { | ||||||||||||||||||||||||||||||||||
score: 0, | ||||||||||||||||||||||||||||||||||
results: [{ | ||||||||||||||||||||||||||||||||||
severity: str_(i18n.UIStrings.itemSeverityHigh), | ||||||||||||||||||||||||||||||||||
description: str_(UIStrings.noClickjackingMitigation), | ||||||||||||||||||||||||||||||||||
directive: undefined, | ||||||||||||||||||||||||||||||||||
}], | ||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||
* @param {LH.Artifacts} artifacts | ||||||||||||||||||||||||||||||||||
* @param {LH.Audit.Context} context | ||||||||||||||||||||||||||||||||||
* @return {Promise<LH.Audit.Product>} | ||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||
static async audit(artifacts, context) { | ||||||||||||||||||||||||||||||||||
const {cspHeaders, xfoHeaders} = await this.getRawCspsAndXfo(artifacts, context); | ||||||||||||||||||||||||||||||||||
const {score, results} = this.constructResults(cspHeaders, xfoHeaders); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
/** @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 ClickjackingMitigation; | ||||||||||||||||||||||||||||||||||
export {UIStrings}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also what makes the XFO simpler to deploy? Seems like they have the same level of complexity to deploy (just modify header values).