diff --git a/cli/test/smokehouse/core-tests.js b/cli/test/smokehouse/core-tests.js index d274770e9bc0..294e73b036ab 100644 --- a/cli/test/smokehouse/core-tests.js +++ b/cli/test/smokehouse/core-tests.js @@ -21,6 +21,8 @@ import fpsMaxPassive from './test-definitions/fps-max-passive.js'; import fpsScaled from './test-definitions/fps-scaled.js'; import fpsOverflowX from './test-definitions/fps-overflow-x.js'; import issuesMixedContent from './test-definitions/issues-mixed-content.js'; +import hstsFullyPresent from './test-definitions/hsts-fully-present.js'; +import hstsMissingDirectives from './test-definitions/hsts-missing-directives.js'; import lanternFetch from './test-definitions/lantern-fetch.js'; import lanternIdleCallbackLong from './test-definitions/lantern-idle-callback-long.js'; import lanternIdleCallbackShort from './test-definitions/lantern-idle-callback-short.js'; @@ -79,6 +81,8 @@ const smokeTests = [ fpsOverflowX, fpsScaled, issuesMixedContent, + hstsFullyPresent, + hstsMissingDirectives, lanternFetch, lanternIdleCallbackLong, lanternIdleCallbackShort, diff --git a/cli/test/smokehouse/test-definitions/hsts-fully-present.js b/cli/test/smokehouse/test-definitions/hsts-fully-present.js new file mode 100644 index 000000000000..a74bf2a78640 --- /dev/null +++ b/cli/test/smokehouse/test-definitions/hsts-fully-present.js @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @type {Smokehouse.ExpectedRunnerResult} + * Expected Lighthouse results a site with full HSTS deployed. + */ +const expectations = { + lhr: { + requestedUrl: 'https://hstspreload.org/', + finalDisplayedUrl: 'https://hstspreload.org/', + audits: { + 'has-hsts': { + score: null, + }, + }, + }, +}; + +export default { + id: 'hsts-fully-present', + expectations, +}; diff --git a/cli/test/smokehouse/test-definitions/hsts-missing-directives.js b/cli/test/smokehouse/test-definitions/hsts-missing-directives.js new file mode 100644 index 000000000000..2fc409b0b784 --- /dev/null +++ b/cli/test/smokehouse/test-definitions/hsts-missing-directives.js @@ -0,0 +1,40 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @type {Smokehouse.ExpectedRunnerResult} + * Expected Lighthouse results a site with HSTS header issues. + */ +const expectations = { + lhr: { + requestedUrl: 'https://developer.mozilla.org/en-US/', + finalDisplayedUrl: 'https://developer.mozilla.org/en-US/', + audits: { + 'has-hsts': { + score: 1, + details: { + items: [ + { + directive: 'includeSubDomains', + description: 'No includeSubDomains directive found', + severity: 'Medium', + }, + { + directive: 'preload', + description: 'No preload directive found', + severity: 'Medium', + } + ] + } + }, + }, + }, +}; + +export default { + id: 'hsts-missing-directives', + expectations, +}; diff --git a/core/audits/has-hsts.js b/core/audits/has-hsts.js new file mode 100644 index 000000000000..98f04cf73d3c --- /dev/null +++ b/core/audits/has-hsts.js @@ -0,0 +1,207 @@ +/** + * @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 HSTS header. "HSTS" stands for "HTTP Strict Transport Security". */ + title: 'Use a strong HSTS policy', + /** Description of a Lighthouse audit that evaluates the security of a page's HSTS header. 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. "HSTS" stands for "HTTP Strict Transport Security". */ + description: 'Deployment of the HSTS header significantly ' + + 'reduces the risk of downgrading of and eavesdropping attacks on HTTP connections. ' + + 'A rollout in stages, starting with a low max-age is recommended. ' + + '[Learn what HSTS is.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security)', + /** Summary text for the results of a Lighthouse audit that evaluates the HSTS header. This is displayed if no HSTS header is deployed. "HSTS" stands for "HTTP Strict Transport Security". */ + noHsts: 'No HSTS header found', + /** Summary text for the results of a Lighthouse audit that evaluates the HSTS header. This is displayed if the preload directive is missing. "HSTS" stands for "HTTP Strict Transport Security". */ + noPreload: 'No preload directive found', + /** Summary text for the results of a Lighthouse audit that evaluates the HSTS header. This is displayed if the includeSubDomains directive is missing. "HSTS" stands for "HTTP Strict Transport Security". */ + noSubdomain: 'No includeSubDomains directive found', + /** Summary text for the results of a Lighthouse audit that evaluates the HSTS header. This is displayed if the max-age directive is missing. "HSTS" stands for "HTTP Strict Transport Security". */ + noMaxAge: 'No max-age directive', + /** Summary text for the results of a Lighthouse audit that evaluates the HSTS header. This is displayed if the provided duration for the max-age directive is too low. "HSTS" stands for "HTTP Strict Transport Security". */ + lowMaxAge: 'max-age is too low', + /** 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 HSTS header. "HSTS" stands for "HTTP Strict Transport Security". */ + columnDirective: 'Directive', + /** Label for a column in a data table; entries will be the severity of an issue with the HSTS header. "HSTS" stands for "HTTP Strict Transport Security". */ + columnSeverity: 'Severity', +}; + +const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings); + +class HasHsts extends Audit { + /** + * @return {LH.Audit.Meta} + */ + static get meta() { + return { + id: 'has-hsts', + scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE, + title: str_(UIStrings.title), + description: str_(UIStrings.description), + requiredArtifacts: ['devtoolsLogs', 'URL'], + }; + } + + + /** + * @param {LH.Artifacts} artifacts + * @param {LH.Audit.Context} context + * @return {Promise} + */ + static async getRawHsts(artifacts, context) { + const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS]; + const mainResource = + await MainResource.request({devtoolsLog, URL: artifacts.URL}, context); + + let hstsHeaders = + mainResource.responseHeaders + .filter(h => { + return h.name.toLowerCase() === 'strict-transport-security'; + }) + .flatMap(h => h.value.split(';')); + + // Sanitize the header value / directives. + hstsHeaders = hstsHeaders.map(v => v.toLowerCase().replace(/\s/g, '')); + + return hstsHeaders; + } + + /** + * @param {string} hstsDirective + * @param {LH.IcuMessage | string} findingDescription + * @param {LH.IcuMessage=} severity + * @return {LH.Audit.Details.TableItem} + */ + static findingToTableItem(hstsDirective, findingDescription, severity) { + return { + directive: hstsDirective, + description: findingDescription, + severity, + }; + } + + /** + * @param {string[]} hstsHeaders + * @return {{score: number, results: LH.Audit.Details.TableItem[]}} + */ + static constructResults(hstsHeaders) { + const rawHsts = [...hstsHeaders]; + const allowedDirectives = ['max-age', 'includesubdomains', 'preload']; + const violations = []; + const warnings = []; + const syntax = []; + + if (!rawHsts.length) { + return { + score: 0, + results: [{ + severity: str_(i18n.UIStrings.itemSeverityHigh), + description: str_(UIStrings.noHsts), + directive: undefined, + }], + }; + } + + // No max-age is a violation and renders the HSTS header useless. + if (!hstsHeaders.toString().includes('max-age')) { + violations.push({ + severity: str_(i18n.UIStrings.itemSeverityHigh), + description: str_(UIStrings.noMaxAge), + directive: 'max-age', + }); + } + + if (!hstsHeaders.toString().includes('includesubdomains')){ + // No includeSubdomains might be even wanted. But would be preferred. + warnings.push({ + severity: str_(i18n.UIStrings.itemSeverityMedium), + description: str_(UIStrings.noSubdomain), + directive: 'includeSubDomains', + }); + } + + if (!hstsHeaders.toString().includes('preload')) { + // No preload might be even wanted. But would be preferred. + warnings.push({ + severity: str_(i18n.UIStrings.itemSeverityMedium), + description: str_(UIStrings.noPreload), + directive: 'preload', + }); + } + + for (const actualDirective of hstsHeaders) { + // We recommend 2y max-age. But if it's lower than 1y, it's a violation. + if (actualDirective.includes('max-age') && + parseInt(actualDirective.split('=')[1], 10) < 31536000) { + violations.push({ + severity: str_(i18n.UIStrings.itemSeverityHigh), + description: str_(UIStrings.lowMaxAge), + directive: 'max-age', + }); + } + + // If there is a directive that's not an official HSTS directive. + if (!allowedDirectives.includes(actualDirective) && + !actualDirective.includes('max-age')) { + 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))), + ...warnings.map( + f => this.findingToTableItem( + f.directive, f.description, + str_(i18n.UIStrings.itemSeverityMedium))), + ...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} + */ + static async audit(artifacts, context) { + const hstsHeaders = await this.getRawHsts(artifacts, context); + const {score, results} = this.constructResults(hstsHeaders); + + /** @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 HasHsts; +export {UIStrings}; diff --git a/core/config/default-config.js b/core/config/default-config.js index 2aeb4562cc83..df54bde45e8c 100644 --- a/core/config/default-config.js +++ b/core/config/default-config.js @@ -192,6 +192,7 @@ const defaultConfig = { 'valid-source-maps', 'prioritize-lcp-image', 'csp-xss', + 'has-hsts', 'script-treemap-data', 'accessibility/accesskeys', 'accessibility/aria-allowed-attr', @@ -541,6 +542,7 @@ const defaultConfig = { {id: 'geolocation-on-start', weight: 1, group: 'best-practices-trust-safety'}, {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: 'hidden'}, // User Experience {id: 'paste-preventing-inputs', weight: 3, group: 'best-practices-ux'}, {id: 'image-aspect-ratio', weight: 1, group: 'best-practices-ux'}, diff --git a/core/scripts/i18n/collect-strings.js b/core/scripts/i18n/collect-strings.js index 623d675d2de4..d0f2e3d394a7 100644 --- a/core/scripts/i18n/collect-strings.js +++ b/core/scripts/i18n/collect-strings.js @@ -729,6 +729,8 @@ function checkKnownFixedCollisions(strings) { 'Back/forward cache is disabled due to a keepalive request.', 'Consider uploading your GIF to a service which will make it available to embed as an HTML5 video.', 'Consider uploading your GIF to a service which will make it available to embed as an HTML5 video.', + '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$', @@ -745,6 +747,8 @@ function checkKnownFixedCollisions(strings) { 'Pages with an in-flight network request are not currently eligible for back/forward cache.', 'Potential Savings', 'Potential Savings', + '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$', diff --git a/core/test/audits/has-hsts-test.js b/core/test/audits/has-hsts-test.js new file mode 100644 index 000000000000..e99aad3f3a5c --- /dev/null +++ b/core/test/audits/has-hsts-test.js @@ -0,0 +1,401 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import HasHsts from '../../audits/has-hsts.js'; +import {networkRecordsToDevtoolsLog} from '../network-records-to-devtools-log.js'; + +it('marked N/A if no violations found', async () => { + const artifacts = { + URL: { + requestedUrl: 'https://example.com', + mainDocumentUrl: 'https://example.com', + finalDisplayedUrl: 'https://example.com', + }, + devtoolsLogs: { + defaultPass: networkRecordsToDevtoolsLog([ + { + url: 'https://example.com', + responseHeaders: [ + { + name: 'Strict-Transport-Security', + value: `max-age=63072000; includeSubDomains; preload`, + }, + ], + }, + ]), + }, + }; + const results = await HasHsts.audit(artifacts, {computedCache: new Map()}); + expect(results.details.items).toHaveLength(0); + expect(results.notApplicable).toBeTruthy(); +}); + +it('max-age missing, but other directives present', async () => { + const artifacts = { + devtoolsLogs: { + defaultPass: networkRecordsToDevtoolsLog([ + { + url: 'https://example.com', + responseHeaders: [ + { + name: 'Strict-Transport-Security', + value: `includeSubDomains; preload`, + }, + ], + }, + ]), + }, + URL: { + requestedUrl: 'https://example.com', + mainDocumentUrl: 'https://example.com', + finalDisplayedUrl: 'https://example.com', + }, + }; + + const results = await HasHsts.audit(artifacts, {computedCache: new Map()}); + expect(results.notApplicable).toBeFalsy(); + expect(results.details.items[0].severity).toBeDisplayString('High'); + expect(results.details.items[0].description) + .toBeDisplayString('No max-age directive'); + expect(results.details.items).toMatchObject([ + { + directive: 'max-age', + }, + ]); +}); + +it('max-age too low, but other directives present', async () => { + const artifacts = { + devtoolsLogs: { + defaultPass: networkRecordsToDevtoolsLog([ + { + url: 'https://example.com', + responseHeaders: [ + { + name: 'Strict-Transport-Security', + value: `max-age=1337; includeSubDomains; preload`, + }, + ], + }, + ]), + }, + URL: { + requestedUrl: 'https://example.com', + mainDocumentUrl: 'https://example.com', + finalDisplayedUrl: 'https://example.com', + }, + }; + + const results = await HasHsts.audit(artifacts, {computedCache: new Map()}); + expect(results.notApplicable).toBeFalsy(); + expect(results.details.items[0].severity).toBeDisplayString('High'); + expect(results.details.items[0].description) + .toBeDisplayString('max-age is too low'); + expect(results.details.items).toMatchObject([ + { + directive: 'max-age', + }, + ]); +}); + +it('includeSubDomains missing, but other directives present', async () => { + const artifacts = { + devtoolsLogs: { + defaultPass: networkRecordsToDevtoolsLog([ + { + url: 'https://example.com', + responseHeaders: [ + { + name: 'Strict-Transport-Security', + value: `max-age=63072000; preload`, + }, + ], + }, + ]), + }, + URL: { + requestedUrl: 'https://example.com', + mainDocumentUrl: 'https://example.com', + finalDisplayedUrl: 'https://example.com', + }, + }; + + const results = await HasHsts.audit(artifacts, {computedCache: new Map()}); + expect(results.notApplicable).toBeFalsy(); + expect(results.details.items[0].severity).toBeDisplayString('Medium'); + expect(results.details.items[0].description) + .toBeDisplayString('No includeSubDomains directive found'); + expect(results.details.items).toMatchObject([ + { + directive: 'includeSubDomains', + }, + ]); +}); + +it('preload missing, but other directives present', async () => { + const artifacts = { + devtoolsLogs: { + defaultPass: networkRecordsToDevtoolsLog([ + { + url: 'https://example.com', + responseHeaders: [ + { + name: 'Strict-Transport-Security', + value: `max-age=63072000; includeSubDomains`, + }, + ], + }, + ]), + }, + URL: { + requestedUrl: 'https://example.com', + mainDocumentUrl: 'https://example.com', + finalDisplayedUrl: 'https://example.com', + }, + }; + + const results = await HasHsts.audit(artifacts, {computedCache: new Map()}); + expect(results.notApplicable).toBeFalsy(); + expect(results.details.items[0].severity).toBeDisplayString('Medium'); + expect(results.details.items[0].description) + .toBeDisplayString('No preload directive found'); + expect(results.details.items).toMatchObject([ + { + directive: 'preload', + }, + ]); +}); + +it('No HSTS header found', async () => { + const artifacts = { + devtoolsLogs: { + defaultPass: networkRecordsToDevtoolsLog([ + { + url: 'https://example.com', + responseHeaders: [ + { + name: 'Foo-Header', + value: `max-age=63072000; includeSubDomains; preload`, + }, + ], + }, + ]), + }, + URL: { + requestedUrl: 'https://example.com', + mainDocumentUrl: 'https://example.com', + finalDisplayedUrl: 'https://example.com', + }, + }; + + const results = await HasHsts.audit(artifacts, {computedCache: new Map()}); + expect(results.notApplicable).toBeFalsy(); + expect(results.details.items[0].severity).toBeDisplayString('High'); + expect(results.details.items[0].description) + .toBeDisplayString('No HSTS header found'); + expect(results.details.items).toMatchObject([ + { + directive: undefined, + }, + ]); +}); + +it('Messed up directive, but other actual HSTS directives present.', async () => { + const artifacts = { + devtoolsLogs: { + defaultPass: networkRecordsToDevtoolsLog([ + { + url: 'https://example.com', + responseHeaders: [ + { + name: 'Strict-Transport-Security', + value: + `max-age=63072000; fooDirective; includeSubDomains; preload`, + }, + ], + }, + ]), + }, + URL: { + requestedUrl: 'https://example.com', + mainDocumentUrl: 'https://example.com', + finalDisplayedUrl: 'https://example.com', + }, + }; + + const results = await HasHsts.audit(artifacts, {computedCache: new Map()}); + expect(results.notApplicable).toBeFalsy(); + expect(results.details.items[0].severity).toBeDisplayString('Low'); + expect(results.details.items[0].description) + .toBeDisplayString('Invalid syntax'); + expect(results.details.items).toMatchObject([ + { + directive: 'foodirective', + }, + ]); +}); + +it('Messed up directive and one more directive missing.', async () => { + const artifacts = { + devtoolsLogs: { + defaultPass: networkRecordsToDevtoolsLog([ + { + url: 'https://example.com', + responseHeaders: [ + { + name: 'Strict-Transport-Security', + value: `max-age=63072000; fooDirective; preload`, + }, + ], + }, + ]), + }, + URL: { + requestedUrl: 'https://example.com', + mainDocumentUrl: 'https://example.com', + finalDisplayedUrl: 'https://example.com', + }, + }; + + const results = await HasHsts.audit(artifacts, {computedCache: new Map()}); + expect(results.notApplicable).toBeFalsy(); + expect(results.details.items[0].severity).toBeDisplayString('Medium'); + expect(results.details.items[0].description) + .toBeDisplayString('No includeSubDomains directive found'); + expect(results.details.items[1].severity).toBeDisplayString('Low'); + expect(results.details.items[1].description) + .toBeDisplayString('Invalid syntax'); + expect(results.details.items).toMatchObject([ + { + directive: 'includeSubDomains', + }, + { + directive: 'foodirective', + }, + ]); +}); + +describe('getRawHsts', () => { + it('basic case', async () => { + const artifacts = { + URL: { + requestedUrl: 'https://example.com', + mainDocumentUrl: 'https://example.com', + finalDisplayedUrl: 'https://example.com', + }, + devtoolsLogs: { + defaultPass: networkRecordsToDevtoolsLog([ + { + url: 'https://example.com', + responseHeaders: [ + { + name: 'Strict-Transport-Security', + value: `max-age=63072000; includeSubDomains; preload`, + }, + ], + }, + ]), + }, + }; + const hstsHeaders = + await HasHsts.getRawHsts(artifacts, {computedCache: new Map()}); + expect(hstsHeaders).toEqual([ + `max-age=63072000`, + `includesubdomains`, + `preload`, + ]); + }); + + it('ignore if empty', async () => { + const artifacts = { + URL: { + requestedUrl: 'https://example.com', + mainDocumentUrl: 'https://example.com', + finalDisplayedUrl: 'https://example.com', + }, + devtoolsLogs: { + defaultPass: networkRecordsToDevtoolsLog([ + { + url: 'https://example.com', + responseHeaders: [ + { + name: 'Strict-Transport-Security', + value: ``, + }, + ], + }, + ]), + }, + }; + const hstsHeaders = + await HasHsts.getRawHsts(artifacts, {computedCache: new Map()}); + expect(hstsHeaders).toEqual([ + ``, + ]); + }); + + it('ignore if only whitespace', async () => { + const artifacts = { + URL: { + requestedUrl: 'https://example.com', + mainDocumentUrl: 'https://example.com', + finalDisplayedUrl: 'https://example.com', + }, + devtoolsLogs: { + defaultPass: networkRecordsToDevtoolsLog([ + { + url: 'https://example.com', + responseHeaders: [ + { + name: 'Strict-Transport-Security', + value: ' \t', + }, + ], + }, + ]), + }, + }; + const hstsHeaders = + await HasHsts.getRawHsts(artifacts, {computedCache: new Map()}); + expect(hstsHeaders).toEqual([ + ``, + ]); + }); +}); + +describe('constructResults', () => { + it('passes with no findings', () => { + const {score, results} = HasHsts.constructResults( + ['max-age=31536000', 'includesubdomains', 'preload']); + expect(score).toEqual(1); + expect(results).toEqual([]); + }); + + it('constructs result based on misconfigured HSTS header', () => { + const {score, results} = HasHsts.constructResults( + ['max-age=31536000', 'foo-directive', 'includesubdomains', 'preload']); + expect(score).toEqual(0); + expect(results[0].severity).toBeDisplayString('Low'); + expect(results[0].description).toBeDisplayString('Invalid syntax'); + expect(results).toMatchObject([ + { + directive: 'foo-directive', + }, + ]); + }); + + it('returns single item for no HSTS', () => { + const {score, results} = HasHsts.constructResults([]); + expect(score).toEqual(0); + expect(results[0].severity).toBeDisplayString('High'); + expect(results[0].description).toBeDisplayString('No HSTS header found'); + expect(results).toMatchObject([ + { + directive: undefined, + }, + ]); + }); +}); diff --git a/core/test/fixtures/user-flows/reports/sample-flow-result.json b/core/test/fixtures/user-flows/reports/sample-flow-result.json index 2157f3ff5d93..d7738eb72ad9 100644 --- a/core/test/fixtures/user-flows/reports/sample-flow-result.json +++ b/core/test/fixtures/user-flows/reports/sample-flow-result.json @@ -2104,6 +2104,54 @@ ] } }, + "has-hsts": { + "id": "has-hsts", + "title": "Use a strong HSTS policy", + "description": "Deployment of the HSTS header significantly reduces the risk of downgrading of and eavesdropping attacks on HTTP connections. A rollout in stages, starting with a low max-age is recommended. [Learn what HSTS is.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security)", + "score": 1, + "scoreDisplayMode": "informative", + "details": { + "type": "table", + "headings": [ + { + "key": "description", + "valueType": "text", + "subItemsHeading": { + "key": "description" + }, + "label": "Description" + }, + { + "key": "directive", + "valueType": "code", + "subItemsHeading": { + "key": "directive" + }, + "label": "Directive" + }, + { + "key": "severity", + "valueType": "text", + "subItemsHeading": { + "key": "severity" + }, + "label": "Severity" + } + ], + "items": [ + { + "directive": "includeSubDomains", + "description": "No includeSubDomains directive found", + "severity": "Medium" + }, + { + "directive": "preload", + "description": "No preload directive found", + "severity": "Medium" + } + ] + } + }, "script-treemap-data": { "id": "script-treemap-data", "title": "Script Treemap Data", @@ -4573,6 +4621,10 @@ "weight": 0, "group": "best-practices-trust-safety" }, + { + "id": "has-hsts", + "weight": 0 + }, { "id": "paste-preventing-inputs", "weight": 3, @@ -6148,19 +6200,19 @@ }, { "startTime": 135, - "name": "lh:audit:script-treemap-data", + "name": "lh:audit:has-hsts", "duration": 1, "entryType": "measure" }, { "startTime": 136, - "name": "lh:computed:ModuleDuplication", + "name": "lh:audit:script-treemap-data", "duration": 1, "entryType": "measure" }, { "startTime": 137, - "name": "lh:computed:UnusedJavascriptSummary", + "name": "lh:computed:ModuleDuplication", "duration": 1, "entryType": "measure" }, @@ -6244,690 +6296,696 @@ }, { "startTime": 151, - "name": "lh:audit:accesskeys", + "name": "lh:computed:UnusedJavascriptSummary", "duration": 1, "entryType": "measure" }, { "startTime": 152, - "name": "lh:audit:aria-allowed-attr", + "name": "lh:audit:accesskeys", "duration": 1, "entryType": "measure" }, { "startTime": 153, - "name": "lh:audit:aria-allowed-role", + "name": "lh:audit:aria-allowed-attr", "duration": 1, "entryType": "measure" }, { "startTime": 154, - "name": "lh:audit:aria-command-name", + "name": "lh:audit:aria-allowed-role", "duration": 1, "entryType": "measure" }, { "startTime": 155, - "name": "lh:audit:aria-conditional-attr", + "name": "lh:audit:aria-command-name", "duration": 1, "entryType": "measure" }, { "startTime": 156, - "name": "lh:audit:aria-deprecated-role", + "name": "lh:audit:aria-conditional-attr", "duration": 1, "entryType": "measure" }, { "startTime": 157, - "name": "lh:audit:aria-dialog-name", + "name": "lh:audit:aria-deprecated-role", "duration": 1, "entryType": "measure" }, { "startTime": 158, - "name": "lh:audit:aria-hidden-body", + "name": "lh:audit:aria-dialog-name", "duration": 1, "entryType": "measure" }, { "startTime": 159, - "name": "lh:audit:aria-hidden-focus", + "name": "lh:audit:aria-hidden-body", "duration": 1, "entryType": "measure" }, { "startTime": 160, - "name": "lh:audit:aria-input-field-name", + "name": "lh:audit:aria-hidden-focus", "duration": 1, "entryType": "measure" }, { "startTime": 161, - "name": "lh:audit:aria-meter-name", + "name": "lh:audit:aria-input-field-name", "duration": 1, "entryType": "measure" }, { "startTime": 162, - "name": "lh:audit:aria-progressbar-name", + "name": "lh:audit:aria-meter-name", "duration": 1, "entryType": "measure" }, { "startTime": 163, - "name": "lh:audit:aria-prohibited-attr", + "name": "lh:audit:aria-progressbar-name", "duration": 1, "entryType": "measure" }, { "startTime": 164, - "name": "lh:audit:aria-required-attr", + "name": "lh:audit:aria-prohibited-attr", "duration": 1, "entryType": "measure" }, { "startTime": 165, - "name": "lh:audit:aria-required-children", + "name": "lh:audit:aria-required-attr", "duration": 1, "entryType": "measure" }, { "startTime": 166, - "name": "lh:audit:aria-required-parent", + "name": "lh:audit:aria-required-children", "duration": 1, "entryType": "measure" }, { "startTime": 167, - "name": "lh:audit:aria-roles", + "name": "lh:audit:aria-required-parent", "duration": 1, "entryType": "measure" }, { "startTime": 168, - "name": "lh:audit:aria-text", + "name": "lh:audit:aria-roles", "duration": 1, "entryType": "measure" }, { "startTime": 169, - "name": "lh:audit:aria-toggle-field-name", + "name": "lh:audit:aria-text", "duration": 1, "entryType": "measure" }, { "startTime": 170, - "name": "lh:audit:aria-tooltip-name", + "name": "lh:audit:aria-toggle-field-name", "duration": 1, "entryType": "measure" }, { "startTime": 171, - "name": "lh:audit:aria-treeitem-name", + "name": "lh:audit:aria-tooltip-name", "duration": 1, "entryType": "measure" }, { "startTime": 172, - "name": "lh:audit:aria-valid-attr-value", + "name": "lh:audit:aria-treeitem-name", "duration": 1, "entryType": "measure" }, { "startTime": 173, - "name": "lh:audit:aria-valid-attr", + "name": "lh:audit:aria-valid-attr-value", "duration": 1, "entryType": "measure" }, { "startTime": 174, - "name": "lh:audit:button-name", + "name": "lh:audit:aria-valid-attr", "duration": 1, "entryType": "measure" }, { "startTime": 175, - "name": "lh:audit:bypass", + "name": "lh:audit:button-name", "duration": 1, "entryType": "measure" }, { "startTime": 176, - "name": "lh:audit:color-contrast", + "name": "lh:audit:bypass", "duration": 1, "entryType": "measure" }, { "startTime": 177, - "name": "lh:audit:definition-list", + "name": "lh:audit:color-contrast", "duration": 1, "entryType": "measure" }, { "startTime": 178, - "name": "lh:audit:dlitem", + "name": "lh:audit:definition-list", "duration": 1, "entryType": "measure" }, { "startTime": 179, - "name": "lh:audit:document-title", + "name": "lh:audit:dlitem", "duration": 1, "entryType": "measure" }, { "startTime": 180, - "name": "lh:audit:duplicate-id-aria", + "name": "lh:audit:document-title", "duration": 1, "entryType": "measure" }, { "startTime": 181, - "name": "lh:audit:empty-heading", + "name": "lh:audit:duplicate-id-aria", "duration": 1, "entryType": "measure" }, { "startTime": 182, - "name": "lh:audit:form-field-multiple-labels", + "name": "lh:audit:empty-heading", "duration": 1, "entryType": "measure" }, { "startTime": 183, - "name": "lh:audit:frame-title", + "name": "lh:audit:form-field-multiple-labels", "duration": 1, "entryType": "measure" }, { "startTime": 184, - "name": "lh:audit:heading-order", + "name": "lh:audit:frame-title", "duration": 1, "entryType": "measure" }, { "startTime": 185, - "name": "lh:audit:html-has-lang", + "name": "lh:audit:heading-order", "duration": 1, "entryType": "measure" }, { "startTime": 186, - "name": "lh:audit:html-lang-valid", + "name": "lh:audit:html-has-lang", "duration": 1, "entryType": "measure" }, { "startTime": 187, - "name": "lh:audit:html-xml-lang-mismatch", + "name": "lh:audit:html-lang-valid", "duration": 1, "entryType": "measure" }, { "startTime": 188, - "name": "lh:audit:identical-links-same-purpose", + "name": "lh:audit:html-xml-lang-mismatch", "duration": 1, "entryType": "measure" }, { "startTime": 189, - "name": "lh:audit:image-alt", + "name": "lh:audit:identical-links-same-purpose", "duration": 1, "entryType": "measure" }, { "startTime": 190, - "name": "lh:audit:image-redundant-alt", + "name": "lh:audit:image-alt", "duration": 1, "entryType": "measure" }, { "startTime": 191, - "name": "lh:audit:input-button-name", + "name": "lh:audit:image-redundant-alt", "duration": 1, "entryType": "measure" }, { "startTime": 192, - "name": "lh:audit:input-image-alt", + "name": "lh:audit:input-button-name", "duration": 1, "entryType": "measure" }, { "startTime": 193, - "name": "lh:audit:label-content-name-mismatch", + "name": "lh:audit:input-image-alt", "duration": 1, "entryType": "measure" }, { "startTime": 194, - "name": "lh:audit:label", + "name": "lh:audit:label-content-name-mismatch", "duration": 1, "entryType": "measure" }, { "startTime": 195, - "name": "lh:audit:landmark-one-main", + "name": "lh:audit:label", "duration": 1, "entryType": "measure" }, { "startTime": 196, - "name": "lh:audit:link-name", + "name": "lh:audit:landmark-one-main", "duration": 1, "entryType": "measure" }, { "startTime": 197, - "name": "lh:audit:link-in-text-block", + "name": "lh:audit:link-name", "duration": 1, "entryType": "measure" }, { "startTime": 198, - "name": "lh:audit:list", + "name": "lh:audit:link-in-text-block", "duration": 1, "entryType": "measure" }, { "startTime": 199, - "name": "lh:audit:listitem", + "name": "lh:audit:list", "duration": 1, "entryType": "measure" }, { "startTime": 200, - "name": "lh:audit:meta-refresh", + "name": "lh:audit:listitem", "duration": 1, "entryType": "measure" }, { "startTime": 201, - "name": "lh:audit:meta-viewport", + "name": "lh:audit:meta-refresh", "duration": 1, "entryType": "measure" }, { "startTime": 202, - "name": "lh:audit:object-alt", + "name": "lh:audit:meta-viewport", "duration": 1, "entryType": "measure" }, { "startTime": 203, - "name": "lh:audit:select-name", + "name": "lh:audit:object-alt", "duration": 1, "entryType": "measure" }, { "startTime": 204, - "name": "lh:audit:skip-link", + "name": "lh:audit:select-name", "duration": 1, "entryType": "measure" }, { "startTime": 205, - "name": "lh:audit:tabindex", + "name": "lh:audit:skip-link", "duration": 1, "entryType": "measure" }, { "startTime": 206, - "name": "lh:audit:table-duplicate-name", + "name": "lh:audit:tabindex", "duration": 1, "entryType": "measure" }, { "startTime": 207, - "name": "lh:audit:table-fake-caption", + "name": "lh:audit:table-duplicate-name", "duration": 1, "entryType": "measure" }, { "startTime": 208, - "name": "lh:audit:target-size", + "name": "lh:audit:table-fake-caption", "duration": 1, "entryType": "measure" }, { "startTime": 209, - "name": "lh:audit:td-has-header", + "name": "lh:audit:target-size", "duration": 1, "entryType": "measure" }, { "startTime": 210, - "name": "lh:audit:td-headers-attr", + "name": "lh:audit:td-has-header", "duration": 1, "entryType": "measure" }, { "startTime": 211, - "name": "lh:audit:th-has-data-cells", + "name": "lh:audit:td-headers-attr", "duration": 1, "entryType": "measure" }, { "startTime": 212, - "name": "lh:audit:valid-lang", + "name": "lh:audit:th-has-data-cells", "duration": 1, "entryType": "measure" }, { "startTime": 213, - "name": "lh:audit:video-caption", + "name": "lh:audit:valid-lang", "duration": 1, "entryType": "measure" }, { "startTime": 214, - "name": "lh:audit:custom-controls-labels", + "name": "lh:audit:video-caption", "duration": 1, "entryType": "measure" }, { "startTime": 215, - "name": "lh:audit:custom-controls-roles", + "name": "lh:audit:custom-controls-labels", "duration": 1, "entryType": "measure" }, { "startTime": 216, - "name": "lh:audit:focus-traps", + "name": "lh:audit:custom-controls-roles", "duration": 1, "entryType": "measure" }, { "startTime": 217, - "name": "lh:audit:focusable-controls", + "name": "lh:audit:focus-traps", "duration": 1, "entryType": "measure" }, { "startTime": 218, - "name": "lh:audit:interactive-element-affordance", + "name": "lh:audit:focusable-controls", "duration": 1, "entryType": "measure" }, { "startTime": 219, - "name": "lh:audit:logical-tab-order", + "name": "lh:audit:interactive-element-affordance", "duration": 1, "entryType": "measure" }, { "startTime": 220, - "name": "lh:audit:managed-focus", + "name": "lh:audit:logical-tab-order", "duration": 1, "entryType": "measure" }, { "startTime": 221, - "name": "lh:audit:offscreen-content-hidden", + "name": "lh:audit:managed-focus", "duration": 1, "entryType": "measure" }, { "startTime": 222, - "name": "lh:audit:use-landmarks", + "name": "lh:audit:offscreen-content-hidden", "duration": 1, "entryType": "measure" }, { "startTime": 223, - "name": "lh:audit:visual-order-follows-dom", + "name": "lh:audit:use-landmarks", "duration": 1, "entryType": "measure" }, { "startTime": 224, - "name": "lh:audit:uses-long-cache-ttl", + "name": "lh:audit:visual-order-follows-dom", "duration": 1, "entryType": "measure" }, { "startTime": 225, - "name": "lh:audit:total-byte-weight", + "name": "lh:audit:uses-long-cache-ttl", "duration": 1, "entryType": "measure" }, { "startTime": 226, - "name": "lh:audit:offscreen-images", + "name": "lh:audit:total-byte-weight", "duration": 1, "entryType": "measure" }, { "startTime": 227, - "name": "lh:audit:render-blocking-resources", + "name": "lh:audit:offscreen-images", "duration": 1, "entryType": "measure" }, { "startTime": 228, - "name": "lh:computed:UnusedCSS", + "name": "lh:audit:render-blocking-resources", "duration": 1, "entryType": "measure" }, { "startTime": 229, - "name": "lh:computed:NavigationInsights", + "name": "lh:computed:UnusedCSS", "duration": 1, "entryType": "measure" }, { "startTime": 230, - "name": "lh:computed:FirstContentfulPaint", + "name": "lh:computed:NavigationInsights", "duration": 1, "entryType": "measure" }, { "startTime": 231, - "name": "lh:audit:unminified-css", + "name": "lh:computed:FirstContentfulPaint", "duration": 1, "entryType": "measure" }, { "startTime": 232, - "name": "lh:audit:unminified-javascript", + "name": "lh:audit:unminified-css", "duration": 1, "entryType": "measure" }, { "startTime": 233, - "name": "lh:audit:unused-css-rules", + "name": "lh:audit:unminified-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 234, - "name": "lh:audit:unused-javascript", + "name": "lh:audit:unused-css-rules", "duration": 1, "entryType": "measure" }, { "startTime": 235, - "name": "lh:audit:modern-image-formats", + "name": "lh:audit:unused-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 236, - "name": "lh:audit:uses-optimized-images", + "name": "lh:audit:modern-image-formats", "duration": 1, "entryType": "measure" }, { "startTime": 237, - "name": "lh:audit:uses-text-compression", + "name": "lh:audit:uses-optimized-images", "duration": 1, "entryType": "measure" }, { "startTime": 238, - "name": "lh:audit:uses-responsive-images", + "name": "lh:audit:uses-text-compression", "duration": 1, "entryType": "measure" }, { "startTime": 239, - "name": "lh:computed:ImageRecords", + "name": "lh:audit:uses-responsive-images", "duration": 1, "entryType": "measure" }, { "startTime": 240, - "name": "lh:audit:efficient-animated-content", + "name": "lh:computed:ImageRecords", "duration": 1, "entryType": "measure" }, { "startTime": 241, - "name": "lh:audit:duplicated-javascript", + "name": "lh:audit:efficient-animated-content", "duration": 1, "entryType": "measure" }, { "startTime": 242, - "name": "lh:audit:legacy-javascript", + "name": "lh:audit:duplicated-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 243, - "name": "lh:audit:doctype", + "name": "lh:audit:legacy-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 244, - "name": "lh:audit:charset", + "name": "lh:audit:doctype", "duration": 1, "entryType": "measure" }, { "startTime": 245, - "name": "lh:audit:dom-size", + "name": "lh:audit:charset", "duration": 1, "entryType": "measure" }, { "startTime": 246, - "name": "lh:audit:geolocation-on-start", + "name": "lh:audit:dom-size", "duration": 1, "entryType": "measure" }, { "startTime": 247, - "name": "lh:audit:inspector-issues", + "name": "lh:audit:geolocation-on-start", "duration": 1, "entryType": "measure" }, { "startTime": 248, - "name": "lh:audit:no-document-write", + "name": "lh:audit:inspector-issues", "duration": 1, "entryType": "measure" }, { "startTime": 249, - "name": "lh:audit:js-libraries", + "name": "lh:audit:no-document-write", "duration": 1, "entryType": "measure" }, { "startTime": 250, - "name": "lh:audit:notification-on-start", + "name": "lh:audit:js-libraries", "duration": 1, "entryType": "measure" }, { "startTime": 251, - "name": "lh:audit:paste-preventing-inputs", + "name": "lh:audit:notification-on-start", "duration": 1, "entryType": "measure" }, { "startTime": 252, - "name": "lh:audit:uses-passive-event-listeners", + "name": "lh:audit:paste-preventing-inputs", "duration": 1, "entryType": "measure" }, { "startTime": 253, - "name": "lh:audit:meta-description", + "name": "lh:audit:uses-passive-event-listeners", "duration": 1, "entryType": "measure" }, { "startTime": 254, - "name": "lh:audit:http-status-code", + "name": "lh:audit:meta-description", "duration": 1, "entryType": "measure" }, { "startTime": 255, - "name": "lh:audit:font-size", + "name": "lh:audit:http-status-code", "duration": 1, "entryType": "measure" }, { "startTime": 256, - "name": "lh:audit:link-text", + "name": "lh:audit:font-size", "duration": 1, "entryType": "measure" }, { "startTime": 257, - "name": "lh:audit:crawlable-anchors", + "name": "lh:audit:link-text", "duration": 1, "entryType": "measure" }, { "startTime": 258, - "name": "lh:audit:is-crawlable", + "name": "lh:audit:crawlable-anchors", "duration": 1, "entryType": "measure" }, { "startTime": 259, - "name": "lh:audit:robots-txt", + "name": "lh:audit:is-crawlable", "duration": 1, "entryType": "measure" }, { "startTime": 260, - "name": "lh:audit:hreflang", + "name": "lh:audit:robots-txt", "duration": 1, "entryType": "measure" }, { "startTime": 261, - "name": "lh:audit:canonical", + "name": "lh:audit:hreflang", "duration": 1, "entryType": "measure" }, { "startTime": 262, - "name": "lh:audit:structured-data", + "name": "lh:audit:canonical", "duration": 1, "entryType": "measure" }, { "startTime": 263, - "name": "lh:audit:bf-cache", + "name": "lh:audit:structured-data", "duration": 1, "entryType": "measure" }, { "startTime": 264, + "name": "lh:audit:bf-cache", + "duration": 1, + "entryType": "measure" + }, + { + "startTime": 265, "name": "lh:runner:generate", "duration": 1, "entryType": "measure" } ], - "total": 265 + "total": 266 }, "i18n": { "rendererFormattedStrings": { @@ -7470,7 +7528,8 @@ "audits[csp-xss].description" ], "core/lib/i18n/i18n.js | columnDescription": [ - "audits[csp-xss].details.headings[0].label" + "audits[csp-xss].details.headings[0].label", + "audits[has-hsts].details.headings[0].label" ], "core/audits/csp-xss.js | columnDirective": [ "audits[csp-xss].details.headings[1].label" @@ -7484,6 +7543,28 @@ "core/audits/csp-xss.js | noCsp": [ "audits[csp-xss].details.items[0].description" ], + "core/audits/has-hsts.js | title": [ + "audits[has-hsts].title" + ], + "core/audits/has-hsts.js | description": [ + "audits[has-hsts].description" + ], + "core/audits/has-hsts.js | columnDirective": [ + "audits[has-hsts].details.headings[1].label" + ], + "core/audits/has-hsts.js | columnSeverity": [ + "audits[has-hsts].details.headings[2].label" + ], + "core/audits/has-hsts.js | noSubdomain": [ + "audits[has-hsts].details.items[0].description" + ], + "core/lib/i18n/i18n.js | itemSeverityMedium": [ + "audits[has-hsts].details.items[0].severity", + "audits[has-hsts].details.items[1].severity" + ], + "core/audits/has-hsts.js | noPreload": [ + "audits[has-hsts].details.items[1].description" + ], "core/audits/accessibility/accesskeys.js | title": [ "audits.accesskeys.title" ], @@ -9750,6 +9831,15 @@ "items": [] } }, + "has-hsts": { + "id": "has-hsts", + "title": "Use a strong HSTS policy", + "description": "Deployment of the HSTS header significantly reduces the risk of downgrading of and eavesdropping attacks on HTTP connections. A rollout in stages, starting with a low max-age is recommended. [Learn what HSTS is.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security)", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "mainDocumentUrl must exist to get the main resource", + "errorStack": "Error: mainDocumentUrl must exist to get the main resource\n at MainResource.compute_ (file:///core/computed/main-resource.js)\n at MainResource.request (file:///core/computed/computed-artifact.js)\n at HasHsts.getRawHsts (file:///core/audits/has-hsts.js)\n at HasHsts.audit (file:///core/audits/has-hsts.js)\n at Runner._runAudit (file:///core/runner.js)\n at Runner._runAudits (file:///core/runner.js)\n at async Runner.audit (file:///core/runner.js)\n at async auditGatherSteps (file:///core/user-flow.js)\n at async Module.auditFlowArtifacts (file:///core/index.js)\n at async generateFlowResult (file:///core/scripts/update-flow-fixtures.js)" + }, "script-treemap-data": { "id": "script-treemap-data", "title": "Script Treemap Data", @@ -10814,6 +10904,10 @@ "weight": 5, "group": "best-practices-trust-safety" }, + { + "id": "has-hsts", + "weight": 0 + }, { "id": "image-aspect-ratio", "weight": 1, @@ -11560,25 +11654,25 @@ }, { "startTime": 77, - "name": "lh:audit:script-treemap-data", + "name": "lh:audit:has-hsts", "duration": 1, "entryType": "measure" }, { "startTime": 78, - "name": "lh:computed:ModuleDuplication", + "name": "lh:computed:MainResource", "duration": 1, "entryType": "measure" }, { "startTime": 79, - "name": "lh:computed:UnusedJavascriptSummary", + "name": "lh:audit:script-treemap-data", "duration": 1, "entryType": "measure" }, { "startTime": 80, - "name": "lh:computed:UnusedJavascriptSummary", + "name": "lh:computed:ModuleDuplication", "duration": 1, "entryType": "measure" }, @@ -11590,126 +11684,138 @@ }, { "startTime": 82, - "name": "lh:audit:uses-long-cache-ttl", + "name": "lh:computed:UnusedJavascriptSummary", "duration": 1, "entryType": "measure" }, { "startTime": 83, - "name": "lh:audit:total-byte-weight", + "name": "lh:computed:UnusedJavascriptSummary", "duration": 1, "entryType": "measure" }, { "startTime": 84, - "name": "lh:audit:unminified-css", + "name": "lh:audit:uses-long-cache-ttl", "duration": 1, "entryType": "measure" }, { "startTime": 85, - "name": "lh:computed:LoadSimulator", + "name": "lh:audit:total-byte-weight", "duration": 1, "entryType": "measure" }, { "startTime": 86, - "name": "lh:audit:unminified-javascript", + "name": "lh:audit:unminified-css", "duration": 1, "entryType": "measure" }, { "startTime": 87, - "name": "lh:audit:unused-javascript", + "name": "lh:computed:LoadSimulator", "duration": 1, "entryType": "measure" }, { "startTime": 88, - "name": "lh:audit:modern-image-formats", + "name": "lh:audit:unminified-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 89, - "name": "lh:audit:uses-optimized-images", + "name": "lh:audit:unused-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 90, - "name": "lh:audit:uses-text-compression", + "name": "lh:audit:modern-image-formats", "duration": 1, "entryType": "measure" }, { "startTime": 91, - "name": "lh:audit:uses-responsive-images", + "name": "lh:audit:uses-optimized-images", "duration": 1, "entryType": "measure" }, { "startTime": 92, - "name": "lh:computed:ImageRecords", + "name": "lh:audit:uses-text-compression", "duration": 1, "entryType": "measure" }, { "startTime": 93, - "name": "lh:audit:efficient-animated-content", + "name": "lh:audit:uses-responsive-images", "duration": 1, "entryType": "measure" }, { "startTime": 94, - "name": "lh:audit:duplicated-javascript", + "name": "lh:computed:ImageRecords", "duration": 1, "entryType": "measure" }, { "startTime": 95, - "name": "lh:audit:legacy-javascript", + "name": "lh:audit:efficient-animated-content", "duration": 1, "entryType": "measure" }, { "startTime": 96, - "name": "lh:audit:inspector-issues", + "name": "lh:audit:duplicated-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 97, - "name": "lh:audit:no-document-write", + "name": "lh:audit:legacy-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 98, - "name": "lh:audit:uses-passive-event-listeners", + "name": "lh:audit:inspector-issues", "duration": 1, "entryType": "measure" }, { "startTime": 99, - "name": "lh:audit:work-during-interaction", + "name": "lh:audit:no-document-write", "duration": 1, "entryType": "measure" }, { "startTime": 100, - "name": "lh:audit:bf-cache", + "name": "lh:audit:uses-passive-event-listeners", "duration": 1, "entryType": "measure" }, { "startTime": 101, + "name": "lh:audit:work-during-interaction", + "duration": 1, + "entryType": "measure" + }, + { + "startTime": 102, + "name": "lh:audit:bf-cache", + "duration": 1, + "entryType": "measure" + }, + { + "startTime": 103, "name": "lh:runner:generate", "duration": 1, "entryType": "measure" } ], - "total": 102 + "total": 104 }, "i18n": { "rendererFormattedStrings": { @@ -12056,6 +12162,12 @@ "core/audits/valid-source-maps.js | description": [ "audits[valid-source-maps].description" ], + "core/audits/has-hsts.js | title": [ + "audits[has-hsts].title" + ], + "core/audits/has-hsts.js | description": [ + "audits[has-hsts].description" + ], "core/audits/byte-efficiency/uses-long-cache-ttl.js | failureTitle": [ "audits[uses-long-cache-ttl].title" ], @@ -19768,10 +19880,52 @@ }, "guidanceLevel": 4 }, - "csp-xss": { - "id": "csp-xss", - "title": "Ensure CSP is effective against XSS attacks", - "description": "A strong Content Security Policy (CSP) significantly reduces the risk of cross-site scripting (XSS) attacks. [Learn how to use a CSP to prevent XSS](https://developer.chrome.com/docs/lighthouse/best-practices/csp-xss/)", + "csp-xss": { + "id": "csp-xss", + "title": "Ensure CSP is effective against XSS attacks", + "description": "A strong Content Security Policy (CSP) significantly reduces the risk of cross-site scripting (XSS) attacks. [Learn how to use a CSP to prevent XSS](https://developer.chrome.com/docs/lighthouse/best-practices/csp-xss/)", + "score": 1, + "scoreDisplayMode": "informative", + "details": { + "type": "table", + "headings": [ + { + "key": "description", + "valueType": "text", + "subItemsHeading": { + "key": "description" + }, + "label": "Description" + }, + { + "key": "directive", + "valueType": "code", + "subItemsHeading": { + "key": "directive" + }, + "label": "Directive" + }, + { + "key": "severity", + "valueType": "text", + "subItemsHeading": { + "key": "severity" + }, + "label": "Severity" + } + ], + "items": [ + { + "severity": "High", + "description": "No CSP found in enforcement mode" + } + ] + } + }, + "has-hsts": { + "id": "has-hsts", + "title": "Use a strong HSTS policy", + "description": "Deployment of the HSTS header significantly reduces the risk of downgrading of and eavesdropping attacks on HTTP connections. A rollout in stages, starting with a low max-age is recommended. [Learn what HSTS is.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security)", "score": 1, "scoreDisplayMode": "informative", "details": { @@ -19804,8 +19958,14 @@ ], "items": [ { - "severity": "High", - "description": "No CSP found in enforcement mode" + "directive": "includeSubDomains", + "description": "No includeSubDomains directive found", + "severity": "Medium" + }, + { + "directive": "preload", + "description": "No preload directive found", + "severity": "Medium" } ] } @@ -22432,6 +22592,10 @@ "weight": 0, "group": "best-practices-trust-safety" }, + { + "id": "has-hsts", + "weight": 0 + }, { "id": "paste-preventing-inputs", "weight": 3, @@ -24058,19 +24222,19 @@ }, { "startTime": 133, - "name": "lh:audit:script-treemap-data", + "name": "lh:audit:has-hsts", "duration": 1, "entryType": "measure" }, { "startTime": 134, - "name": "lh:computed:ModuleDuplication", + "name": "lh:audit:script-treemap-data", "duration": 1, "entryType": "measure" }, { "startTime": 135, - "name": "lh:computed:UnusedJavascriptSummary", + "name": "lh:computed:ModuleDuplication", "duration": 1, "entryType": "measure" }, @@ -24160,690 +24324,696 @@ }, { "startTime": 150, - "name": "lh:audit:accesskeys", + "name": "lh:computed:UnusedJavascriptSummary", "duration": 1, "entryType": "measure" }, { "startTime": 151, - "name": "lh:audit:aria-allowed-attr", + "name": "lh:audit:accesskeys", "duration": 1, "entryType": "measure" }, { "startTime": 152, - "name": "lh:audit:aria-allowed-role", + "name": "lh:audit:aria-allowed-attr", "duration": 1, "entryType": "measure" }, { "startTime": 153, - "name": "lh:audit:aria-command-name", + "name": "lh:audit:aria-allowed-role", "duration": 1, "entryType": "measure" }, { "startTime": 154, - "name": "lh:audit:aria-conditional-attr", + "name": "lh:audit:aria-command-name", "duration": 1, "entryType": "measure" }, { "startTime": 155, - "name": "lh:audit:aria-deprecated-role", + "name": "lh:audit:aria-conditional-attr", "duration": 1, "entryType": "measure" }, { "startTime": 156, - "name": "lh:audit:aria-dialog-name", + "name": "lh:audit:aria-deprecated-role", "duration": 1, "entryType": "measure" }, { "startTime": 157, - "name": "lh:audit:aria-hidden-body", + "name": "lh:audit:aria-dialog-name", "duration": 1, "entryType": "measure" }, { "startTime": 158, - "name": "lh:audit:aria-hidden-focus", + "name": "lh:audit:aria-hidden-body", "duration": 1, "entryType": "measure" }, { "startTime": 159, - "name": "lh:audit:aria-input-field-name", + "name": "lh:audit:aria-hidden-focus", "duration": 1, "entryType": "measure" }, { "startTime": 160, - "name": "lh:audit:aria-meter-name", + "name": "lh:audit:aria-input-field-name", "duration": 1, "entryType": "measure" }, { "startTime": 161, - "name": "lh:audit:aria-progressbar-name", + "name": "lh:audit:aria-meter-name", "duration": 1, "entryType": "measure" }, { "startTime": 162, - "name": "lh:audit:aria-prohibited-attr", + "name": "lh:audit:aria-progressbar-name", "duration": 1, "entryType": "measure" }, { "startTime": 163, - "name": "lh:audit:aria-required-attr", + "name": "lh:audit:aria-prohibited-attr", "duration": 1, "entryType": "measure" }, { "startTime": 164, - "name": "lh:audit:aria-required-children", + "name": "lh:audit:aria-required-attr", "duration": 1, "entryType": "measure" }, { "startTime": 165, - "name": "lh:audit:aria-required-parent", + "name": "lh:audit:aria-required-children", "duration": 1, "entryType": "measure" }, { "startTime": 166, - "name": "lh:audit:aria-roles", + "name": "lh:audit:aria-required-parent", "duration": 1, "entryType": "measure" }, { "startTime": 167, - "name": "lh:audit:aria-text", + "name": "lh:audit:aria-roles", "duration": 1, "entryType": "measure" }, { "startTime": 168, - "name": "lh:audit:aria-toggle-field-name", + "name": "lh:audit:aria-text", "duration": 1, "entryType": "measure" }, { "startTime": 169, - "name": "lh:audit:aria-tooltip-name", + "name": "lh:audit:aria-toggle-field-name", "duration": 1, "entryType": "measure" }, { "startTime": 170, - "name": "lh:audit:aria-treeitem-name", + "name": "lh:audit:aria-tooltip-name", "duration": 1, "entryType": "measure" }, { "startTime": 171, - "name": "lh:audit:aria-valid-attr-value", + "name": "lh:audit:aria-treeitem-name", "duration": 1, "entryType": "measure" }, { "startTime": 172, - "name": "lh:audit:aria-valid-attr", + "name": "lh:audit:aria-valid-attr-value", "duration": 1, "entryType": "measure" }, { "startTime": 173, - "name": "lh:audit:button-name", + "name": "lh:audit:aria-valid-attr", "duration": 1, "entryType": "measure" }, { "startTime": 174, - "name": "lh:audit:bypass", + "name": "lh:audit:button-name", "duration": 1, "entryType": "measure" }, { "startTime": 175, - "name": "lh:audit:color-contrast", + "name": "lh:audit:bypass", "duration": 1, "entryType": "measure" }, { "startTime": 176, - "name": "lh:audit:definition-list", + "name": "lh:audit:color-contrast", "duration": 1, "entryType": "measure" }, { "startTime": 177, - "name": "lh:audit:dlitem", + "name": "lh:audit:definition-list", "duration": 1, "entryType": "measure" }, { "startTime": 178, - "name": "lh:audit:document-title", + "name": "lh:audit:dlitem", "duration": 1, "entryType": "measure" }, { "startTime": 179, - "name": "lh:audit:duplicate-id-aria", + "name": "lh:audit:document-title", "duration": 1, "entryType": "measure" }, { "startTime": 180, - "name": "lh:audit:empty-heading", + "name": "lh:audit:duplicate-id-aria", "duration": 1, "entryType": "measure" }, { "startTime": 181, - "name": "lh:audit:form-field-multiple-labels", + "name": "lh:audit:empty-heading", "duration": 1, "entryType": "measure" }, { "startTime": 182, - "name": "lh:audit:frame-title", + "name": "lh:audit:form-field-multiple-labels", "duration": 1, "entryType": "measure" }, { "startTime": 183, - "name": "lh:audit:heading-order", + "name": "lh:audit:frame-title", "duration": 1, "entryType": "measure" }, { "startTime": 184, - "name": "lh:audit:html-has-lang", + "name": "lh:audit:heading-order", "duration": 1, "entryType": "measure" }, { "startTime": 185, - "name": "lh:audit:html-lang-valid", + "name": "lh:audit:html-has-lang", "duration": 1, "entryType": "measure" }, { "startTime": 186, - "name": "lh:audit:html-xml-lang-mismatch", + "name": "lh:audit:html-lang-valid", "duration": 1, "entryType": "measure" }, { "startTime": 187, - "name": "lh:audit:identical-links-same-purpose", + "name": "lh:audit:html-xml-lang-mismatch", "duration": 1, "entryType": "measure" }, { "startTime": 188, - "name": "lh:audit:image-alt", + "name": "lh:audit:identical-links-same-purpose", "duration": 1, "entryType": "measure" }, { "startTime": 189, - "name": "lh:audit:image-redundant-alt", + "name": "lh:audit:image-alt", "duration": 1, "entryType": "measure" }, { "startTime": 190, - "name": "lh:audit:input-button-name", + "name": "lh:audit:image-redundant-alt", "duration": 1, "entryType": "measure" }, { "startTime": 191, - "name": "lh:audit:input-image-alt", + "name": "lh:audit:input-button-name", "duration": 1, "entryType": "measure" }, { "startTime": 192, - "name": "lh:audit:label-content-name-mismatch", + "name": "lh:audit:input-image-alt", "duration": 1, "entryType": "measure" }, { "startTime": 193, - "name": "lh:audit:label", + "name": "lh:audit:label-content-name-mismatch", "duration": 1, "entryType": "measure" }, { "startTime": 194, - "name": "lh:audit:landmark-one-main", + "name": "lh:audit:label", "duration": 1, "entryType": "measure" }, { "startTime": 195, - "name": "lh:audit:link-name", + "name": "lh:audit:landmark-one-main", "duration": 1, "entryType": "measure" }, { "startTime": 196, - "name": "lh:audit:link-in-text-block", + "name": "lh:audit:link-name", "duration": 1, "entryType": "measure" }, { "startTime": 197, - "name": "lh:audit:list", + "name": "lh:audit:link-in-text-block", "duration": 1, "entryType": "measure" }, { "startTime": 198, - "name": "lh:audit:listitem", + "name": "lh:audit:list", "duration": 1, "entryType": "measure" }, { "startTime": 199, - "name": "lh:audit:meta-refresh", + "name": "lh:audit:listitem", "duration": 1, "entryType": "measure" }, { "startTime": 200, - "name": "lh:audit:meta-viewport", + "name": "lh:audit:meta-refresh", "duration": 1, "entryType": "measure" }, { "startTime": 201, - "name": "lh:audit:object-alt", + "name": "lh:audit:meta-viewport", "duration": 1, "entryType": "measure" }, { "startTime": 202, - "name": "lh:audit:select-name", + "name": "lh:audit:object-alt", "duration": 1, "entryType": "measure" }, { "startTime": 203, - "name": "lh:audit:skip-link", + "name": "lh:audit:select-name", "duration": 1, "entryType": "measure" }, { "startTime": 204, - "name": "lh:audit:tabindex", + "name": "lh:audit:skip-link", "duration": 1, "entryType": "measure" }, { "startTime": 205, - "name": "lh:audit:table-duplicate-name", + "name": "lh:audit:tabindex", "duration": 1, "entryType": "measure" }, { "startTime": 206, - "name": "lh:audit:table-fake-caption", + "name": "lh:audit:table-duplicate-name", "duration": 1, "entryType": "measure" }, { "startTime": 207, - "name": "lh:audit:target-size", + "name": "lh:audit:table-fake-caption", "duration": 1, "entryType": "measure" }, { "startTime": 208, - "name": "lh:audit:td-has-header", + "name": "lh:audit:target-size", "duration": 1, "entryType": "measure" }, { "startTime": 209, - "name": "lh:audit:td-headers-attr", + "name": "lh:audit:td-has-header", "duration": 1, "entryType": "measure" }, { "startTime": 210, - "name": "lh:audit:th-has-data-cells", + "name": "lh:audit:td-headers-attr", "duration": 1, "entryType": "measure" }, { "startTime": 211, - "name": "lh:audit:valid-lang", + "name": "lh:audit:th-has-data-cells", "duration": 1, "entryType": "measure" }, { "startTime": 212, - "name": "lh:audit:video-caption", + "name": "lh:audit:valid-lang", "duration": 1, "entryType": "measure" }, { "startTime": 213, - "name": "lh:audit:custom-controls-labels", + "name": "lh:audit:video-caption", "duration": 1, "entryType": "measure" }, { "startTime": 214, - "name": "lh:audit:custom-controls-roles", + "name": "lh:audit:custom-controls-labels", "duration": 1, "entryType": "measure" }, { "startTime": 215, - "name": "lh:audit:focus-traps", + "name": "lh:audit:custom-controls-roles", "duration": 1, "entryType": "measure" }, { "startTime": 216, - "name": "lh:audit:focusable-controls", + "name": "lh:audit:focus-traps", "duration": 1, "entryType": "measure" }, { "startTime": 217, - "name": "lh:audit:interactive-element-affordance", + "name": "lh:audit:focusable-controls", "duration": 1, "entryType": "measure" }, { "startTime": 218, - "name": "lh:audit:logical-tab-order", + "name": "lh:audit:interactive-element-affordance", "duration": 1, "entryType": "measure" }, { "startTime": 219, - "name": "lh:audit:managed-focus", + "name": "lh:audit:logical-tab-order", "duration": 1, "entryType": "measure" }, { "startTime": 220, - "name": "lh:audit:offscreen-content-hidden", + "name": "lh:audit:managed-focus", "duration": 1, "entryType": "measure" }, { "startTime": 221, - "name": "lh:audit:use-landmarks", + "name": "lh:audit:offscreen-content-hidden", "duration": 1, "entryType": "measure" }, { "startTime": 222, - "name": "lh:audit:visual-order-follows-dom", + "name": "lh:audit:use-landmarks", "duration": 1, "entryType": "measure" }, { "startTime": 223, - "name": "lh:audit:uses-long-cache-ttl", + "name": "lh:audit:visual-order-follows-dom", "duration": 1, "entryType": "measure" }, { "startTime": 224, - "name": "lh:audit:total-byte-weight", + "name": "lh:audit:uses-long-cache-ttl", "duration": 1, "entryType": "measure" }, { "startTime": 225, - "name": "lh:audit:offscreen-images", + "name": "lh:audit:total-byte-weight", "duration": 1, "entryType": "measure" }, { "startTime": 226, - "name": "lh:audit:render-blocking-resources", + "name": "lh:audit:offscreen-images", "duration": 1, "entryType": "measure" }, { "startTime": 227, - "name": "lh:computed:UnusedCSS", + "name": "lh:audit:render-blocking-resources", "duration": 1, "entryType": "measure" }, { "startTime": 228, - "name": "lh:computed:NavigationInsights", + "name": "lh:computed:UnusedCSS", "duration": 1, "entryType": "measure" }, { "startTime": 229, - "name": "lh:computed:FirstContentfulPaint", + "name": "lh:computed:NavigationInsights", "duration": 1, "entryType": "measure" }, { "startTime": 230, - "name": "lh:audit:unminified-css", + "name": "lh:computed:FirstContentfulPaint", "duration": 1, "entryType": "measure" }, { "startTime": 231, - "name": "lh:audit:unminified-javascript", + "name": "lh:audit:unminified-css", "duration": 1, "entryType": "measure" }, { "startTime": 232, - "name": "lh:audit:unused-css-rules", + "name": "lh:audit:unminified-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 233, - "name": "lh:audit:unused-javascript", + "name": "lh:audit:unused-css-rules", "duration": 1, "entryType": "measure" }, { "startTime": 234, - "name": "lh:audit:modern-image-formats", + "name": "lh:audit:unused-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 235, - "name": "lh:audit:uses-optimized-images", + "name": "lh:audit:modern-image-formats", "duration": 1, "entryType": "measure" }, { "startTime": 236, - "name": "lh:audit:uses-text-compression", + "name": "lh:audit:uses-optimized-images", "duration": 1, "entryType": "measure" }, { "startTime": 237, - "name": "lh:audit:uses-responsive-images", + "name": "lh:audit:uses-text-compression", "duration": 1, "entryType": "measure" }, { "startTime": 238, - "name": "lh:computed:ImageRecords", + "name": "lh:audit:uses-responsive-images", "duration": 1, "entryType": "measure" }, { "startTime": 239, - "name": "lh:audit:efficient-animated-content", + "name": "lh:computed:ImageRecords", "duration": 1, "entryType": "measure" }, { "startTime": 240, - "name": "lh:audit:duplicated-javascript", + "name": "lh:audit:efficient-animated-content", "duration": 1, "entryType": "measure" }, { "startTime": 241, - "name": "lh:audit:legacy-javascript", + "name": "lh:audit:duplicated-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 242, - "name": "lh:audit:doctype", + "name": "lh:audit:legacy-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 243, - "name": "lh:audit:charset", + "name": "lh:audit:doctype", "duration": 1, "entryType": "measure" }, { "startTime": 244, - "name": "lh:audit:dom-size", + "name": "lh:audit:charset", "duration": 1, "entryType": "measure" }, { "startTime": 245, - "name": "lh:audit:geolocation-on-start", + "name": "lh:audit:dom-size", "duration": 1, "entryType": "measure" }, { "startTime": 246, - "name": "lh:audit:inspector-issues", + "name": "lh:audit:geolocation-on-start", "duration": 1, "entryType": "measure" }, { "startTime": 247, - "name": "lh:audit:no-document-write", + "name": "lh:audit:inspector-issues", "duration": 1, "entryType": "measure" }, { "startTime": 248, - "name": "lh:audit:js-libraries", + "name": "lh:audit:no-document-write", "duration": 1, "entryType": "measure" }, { "startTime": 249, - "name": "lh:audit:notification-on-start", + "name": "lh:audit:js-libraries", "duration": 1, "entryType": "measure" }, { "startTime": 250, - "name": "lh:audit:paste-preventing-inputs", + "name": "lh:audit:notification-on-start", "duration": 1, "entryType": "measure" }, { "startTime": 251, - "name": "lh:audit:uses-passive-event-listeners", + "name": "lh:audit:paste-preventing-inputs", "duration": 1, "entryType": "measure" }, { "startTime": 252, - "name": "lh:audit:meta-description", + "name": "lh:audit:uses-passive-event-listeners", "duration": 1, "entryType": "measure" }, { "startTime": 253, - "name": "lh:audit:http-status-code", + "name": "lh:audit:meta-description", "duration": 1, "entryType": "measure" }, { "startTime": 254, - "name": "lh:audit:font-size", + "name": "lh:audit:http-status-code", "duration": 1, "entryType": "measure" }, { "startTime": 255, - "name": "lh:audit:link-text", + "name": "lh:audit:font-size", "duration": 1, "entryType": "measure" }, { "startTime": 256, - "name": "lh:audit:crawlable-anchors", + "name": "lh:audit:link-text", "duration": 1, "entryType": "measure" }, { "startTime": 257, - "name": "lh:audit:is-crawlable", + "name": "lh:audit:crawlable-anchors", "duration": 1, "entryType": "measure" }, { "startTime": 258, - "name": "lh:audit:robots-txt", + "name": "lh:audit:is-crawlable", "duration": 1, "entryType": "measure" }, { "startTime": 259, - "name": "lh:audit:hreflang", + "name": "lh:audit:robots-txt", "duration": 1, "entryType": "measure" }, { "startTime": 260, - "name": "lh:audit:canonical", + "name": "lh:audit:hreflang", "duration": 1, "entryType": "measure" }, { "startTime": 261, - "name": "lh:audit:structured-data", + "name": "lh:audit:canonical", "duration": 1, "entryType": "measure" }, { "startTime": 262, - "name": "lh:audit:bf-cache", + "name": "lh:audit:structured-data", "duration": 1, "entryType": "measure" }, { "startTime": 263, + "name": "lh:audit:bf-cache", + "duration": 1, + "entryType": "measure" + }, + { + "startTime": 264, "name": "lh:runner:generate", "duration": 1, "entryType": "measure" } ], - "total": 264 + "total": 265 }, "i18n": { "rendererFormattedStrings": { @@ -25364,7 +25534,8 @@ "audits[csp-xss].description" ], "core/lib/i18n/i18n.js | columnDescription": [ - "audits[csp-xss].details.headings[0].label" + "audits[csp-xss].details.headings[0].label", + "audits[has-hsts].details.headings[0].label" ], "core/audits/csp-xss.js | columnDirective": [ "audits[csp-xss].details.headings[1].label" @@ -25378,6 +25549,28 @@ "core/audits/csp-xss.js | noCsp": [ "audits[csp-xss].details.items[0].description" ], + "core/audits/has-hsts.js | title": [ + "audits[has-hsts].title" + ], + "core/audits/has-hsts.js | description": [ + "audits[has-hsts].description" + ], + "core/audits/has-hsts.js | columnDirective": [ + "audits[has-hsts].details.headings[1].label" + ], + "core/audits/has-hsts.js | columnSeverity": [ + "audits[has-hsts].details.headings[2].label" + ], + "core/audits/has-hsts.js | noSubdomain": [ + "audits[has-hsts].details.items[0].description" + ], + "core/lib/i18n/i18n.js | itemSeverityMedium": [ + "audits[has-hsts].details.items[0].severity", + "audits[has-hsts].details.items[1].severity" + ], + "core/audits/has-hsts.js | noPreload": [ + "audits[has-hsts].details.items[1].description" + ], "core/audits/accessibility/accesskeys.js | title": [ "audits.accesskeys.title" ], diff --git a/core/test/results/sample_v2.json b/core/test/results/sample_v2.json index 3f3c9a3526dd..9cdb808c162f 100644 --- a/core/test/results/sample_v2.json +++ b/core/test/results/sample_v2.json @@ -2765,6 +2765,48 @@ ] } }, + "has-hsts": { + "id": "has-hsts", + "title": "Use a strong HSTS policy", + "description": "Deployment of the HSTS header significantly reduces the risk of downgrading of and eavesdropping attacks on HTTP connections. A rollout in stages, starting with a low max-age is recommended. [Learn what HSTS is.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security)", + "score": 1, + "scoreDisplayMode": "informative", + "details": { + "type": "table", + "headings": [ + { + "key": "description", + "valueType": "text", + "subItemsHeading": { + "key": "description" + }, + "label": "Description" + }, + { + "key": "directive", + "valueType": "code", + "subItemsHeading": { + "key": "directive" + }, + "label": "Directive" + }, + { + "key": "severity", + "valueType": "text", + "subItemsHeading": { + "key": "severity" + }, + "label": "Severity" + } + ], + "items": [ + { + "severity": "High", + "description": "No HSTS header found" + } + ] + } + }, "script-treemap-data": { "id": "script-treemap-data", "title": "Script Treemap Data", @@ -6709,6 +6751,10 @@ "weight": 0, "group": "best-practices-trust-safety" }, + { + "id": "has-hsts", + "weight": 0 + }, { "id": "paste-preventing-inputs", "weight": 3, @@ -8150,6 +8196,12 @@ "duration": 100, "entryType": "measure" }, + { + "startTime": 0, + "name": "lh:audit:has-hsts", + "duration": 100, + "entryType": "measure" + }, { "startTime": 0, "name": "lh:audit:script-treemap-data", @@ -9201,7 +9253,8 @@ ], "core/lib/i18n/i18n.js | columnDescription": [ "audits[errors-in-console].details.headings[1].label", - "audits[csp-xss].details.headings[0].label" + "audits[csp-xss].details.headings[0].label", + "audits[has-hsts].details.headings[0].label" ], "core/audits/server-response-time.js | title": [ "audits[server-response-time].title" @@ -9622,11 +9675,27 @@ "audits[csp-xss].details.headings[2].label" ], "core/lib/i18n/i18n.js | itemSeverityHigh": [ - "audits[csp-xss].details.items[0].severity" + "audits[csp-xss].details.items[0].severity", + "audits[has-hsts].details.items[0].severity" ], "core/audits/csp-xss.js | noCsp": [ "audits[csp-xss].details.items[0].description" ], + "core/audits/has-hsts.js | title": [ + "audits[has-hsts].title" + ], + "core/audits/has-hsts.js | description": [ + "audits[has-hsts].description" + ], + "core/audits/has-hsts.js | columnDirective": [ + "audits[has-hsts].details.headings[1].label" + ], + "core/audits/has-hsts.js | columnSeverity": [ + "audits[has-hsts].details.headings[2].label" + ], + "core/audits/has-hsts.js | noHsts": [ + "audits[has-hsts].details.items[0].description" + ], "core/audits/accessibility/accesskeys.js | title": [ "audits.accesskeys.title" ], diff --git a/shared/localization/locales/en-US.json b/shared/localization/locales/en-US.json index 2c72b68635df..59389fbe61a8 100644 --- a/shared/localization/locales/en-US.json +++ b/shared/localization/locales/en-US.json @@ -977,6 +977,36 @@ "core/audits/font-display.js | undeclaredFontOriginWarning": { "message": "{fontCountForOrigin, plural, =1 {Lighthouse was unable to automatically check the `font-display` value for the origin {fontOrigin}.} other {Lighthouse was unable to automatically check the `font-display` values for the origin {fontOrigin}.}}" }, + "core/audits/has-hsts.js | columnDirective": { + "message": "Directive" + }, + "core/audits/has-hsts.js | columnSeverity": { + "message": "Severity" + }, + "core/audits/has-hsts.js | description": { + "message": "Deployment of the HSTS header significantly reduces the risk of downgrading of and eavesdropping attacks on HTTP connections. A rollout in stages, starting with a low max-age is recommended. [Learn what HSTS is.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security)" + }, + "core/audits/has-hsts.js | invalidSyntax": { + "message": "Invalid syntax" + }, + "core/audits/has-hsts.js | lowMaxAge": { + "message": "max-age is too low" + }, + "core/audits/has-hsts.js | noHsts": { + "message": "No HSTS header found" + }, + "core/audits/has-hsts.js | noMaxAge": { + "message": "No max-age directive" + }, + "core/audits/has-hsts.js | noPreload": { + "message": "No preload directive found" + }, + "core/audits/has-hsts.js | noSubdomain": { + "message": "No includeSubDomains directive found" + }, + "core/audits/has-hsts.js | title": { + "message": "Use a strong HSTS policy" + }, "core/audits/image-aspect-ratio.js | columnActual": { "message": "Aspect Ratio (Actual)" }, diff --git a/shared/localization/locales/en-XL.json b/shared/localization/locales/en-XL.json index 48cea84a1de6..ac2cfefe88e9 100644 --- a/shared/localization/locales/en-XL.json +++ b/shared/localization/locales/en-XL.json @@ -977,6 +977,36 @@ "core/audits/font-display.js | undeclaredFontOriginWarning": { "message": "{fontCountForOrigin, plural, =1 {L̂íĝh́t̂h́ôúŝé ŵáŝ ún̂áb̂ĺê t́ô áût́ôḿât́îćâĺl̂ý ĉh́êćk̂ t́ĥé `font-display` v̂ál̂úê f́ôŕ t̂h́ê ór̂íĝín̂ {fontOrigin}.} other {Ĺîǵĥt́ĥóûśê ẃâś ûńâb́l̂é t̂ó âút̂óm̂át̂íĉál̂ĺŷ ćĥéĉḱ t̂h́ê `font-display` v́âĺûéŝ f́ôŕ t̂h́ê ór̂íĝín̂ {fontOrigin}.}}" }, + "core/audits/has-hsts.js | columnDirective": { + "message": "D̂ír̂éĉt́îv́ê" + }, + "core/audits/has-hsts.js | columnSeverity": { + "message": "Ŝév̂ér̂ít̂ý" + }, + "core/audits/has-hsts.js | description": { + "message": "D̂ép̂ĺôým̂én̂t́ ôf́ t̂h́ê H́ŜT́Ŝ h́êád̂ér̂ śîǵn̂íf̂íĉán̂t́l̂ý r̂éd̂úĉéŝ t́ĥé r̂íŝḱ ôf́ d̂óŵńĝŕâd́îńĝ óf̂ án̂d́ êáv̂éŝd́r̂óp̂ṕîńĝ át̂t́âćk̂ś ôń ĤT́T̂Ṕ ĉón̂ńêćt̂íôńŝ. Á r̂ól̂ĺôút̂ ín̂ śt̂áĝéŝ, śt̂ár̂t́îńĝ ẃît́ĥ á l̂óŵ ḿâx́-âǵê íŝ ŕêćôḿm̂én̂d́êd́. [L̂éâŕn̂ ẃĥát̂ H́ŜT́Ŝ íŝ.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security)" + }, + "core/audits/has-hsts.js | invalidSyntax": { + "message": "Îńv̂ál̂íd̂ śŷńt̂áx̂" + }, + "core/audits/has-hsts.js | lowMaxAge": { + "message": "m̂áx̂-áĝé îś t̂óô ĺôẃ" + }, + "core/audits/has-hsts.js | noHsts": { + "message": "N̂ó ĤŚT̂Ś ĥéâd́êŕ f̂óûńd̂" + }, + "core/audits/has-hsts.js | noMaxAge": { + "message": "N̂ó m̂áx̂-áĝé d̂ír̂éĉt́îv́ê" + }, + "core/audits/has-hsts.js | noPreload": { + "message": "N̂ó p̂ŕêĺôád̂ d́îŕêćt̂ív̂é f̂óûńd̂" + }, + "core/audits/has-hsts.js | noSubdomain": { + "message": "N̂ó îńĉĺûd́êŚûb́D̂óm̂áîńŝ d́îŕêćt̂ív̂é f̂óûńd̂" + }, + "core/audits/has-hsts.js | title": { + "message": "Ûśê á ŝt́r̂ón̂ǵ ĤŚT̂Ś p̂ól̂íĉý" + }, "core/audits/image-aspect-ratio.js | columnActual": { "message": "Âśp̂éĉt́ R̂át̂íô (Áĉt́ûál̂)" },