UNPKG

chrome-devtools-frontend

Version:
1,203 lines (1,090 loc) • 71.3 kB
// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* eslint-disable rulesdir/no-imperative-dom-api */ /* eslint-disable rulesdir/no-lit-render-outside-of-view */ import * as Common from '../../core/common/common.js'; import * as Host from '../../core/host/host.js'; import * as i18n from '../../core/i18n/i18n.js'; import type * as Platform from '../../core/platform/platform.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Protocol from '../../generated/protocol.js'; import * as NetworkForward from '../../panels/network/forward/forward.js'; import * as IconButton from '../../ui/components/icon_button/icon_button.js'; import * as UI from '../../ui/legacy/legacy.js'; import {html, render} from '../../ui/lit/lit.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import {CookieControlsView} from './CookieControlsView.js'; import {CookieReportView} from './CookieReportView.js'; import lockIconStyles from './lockIcon.css.js'; import mainViewStyles from './mainView.css.js'; import {ShowOriginEvent} from './OriginTreeElement.js'; import originViewStyles from './originView.css.js'; import { Events, type PageVisibleSecurityState, SecurityModel, securityStateCompare, SecurityStyleExplanation, SummaryMessages, } from './SecurityModel.js'; import {SecurityPanelSidebar} from './SecurityPanelSidebar.js'; const {widgetConfig} = UI.Widget; const UIStrings = { /** *@description Summary div text content in Security Panel of the Security panel */ securityOverview: 'Security overview', /** *@description Text to show something is secure */ secure: 'Secure', /** *@description Sdk console message message level info of level Labels in Console View of the Console panel */ info: 'Info', /** *@description Not secure div text content in Security Panel of the Security panel */ notSecure: 'Not secure', /** *@description Text to view a security certificate */ viewCertificate: 'View certificate', /** *@description Text in Security Panel of the Security panel */ notSecureBroken: 'Not secure (broken)', /** *@description Main summary for page when it has been deemed unsafe by the SafeBrowsing service. */ thisPageIsDangerousFlaggedBy: 'This page is dangerous (flagged by Google Safe Browsing).', /** *@description Summary phrase for a security problem where the site is deemed unsafe by the SafeBrowsing service. */ flaggedByGoogleSafeBrowsing: 'Flagged by Google Safe Browsing', /** *@description Description of a security problem where the site is deemed unsafe by the SafeBrowsing service. */ toCheckThisPagesStatusVisit: 'To check this page\'s status, visit g.co/safebrowsingstatus.', /** *@description Main summary for a non cert error page. */ thisIsAnErrorPage: 'This is an error page.', /** *@description Main summary for where the site is non-secure HTTP. */ thisPageIsInsecureUnencrypted: 'This page is insecure (unencrypted HTTP).', /** *@description Main summary for where the site has a non-cryptographic secure origin. */ thisPageHasANonhttpsSecureOrigin: 'This page has a non-HTTPS secure origin.', /** *@description Message to display in devtools security tab when the page you are on triggered a safety tip. */ thisPageIsSuspicious: 'This page is suspicious', /** *@description Body of message to display in devtools security tab when you are viewing a page that triggered a safety tip. */ chromeHasDeterminedThatThisSiteS: 'Chrome has determined that this site could be fake or fraudulent.', /** *@description Second part of the body of message to display in devtools security tab when you are viewing a page that triggered a safety tip. */ ifYouBelieveThisIsShownIn: 'If you believe this is shown in error please visit https://g.co/chrome/lookalike-warnings.', /** *@description Summary of a warning when the user visits a page that triggered a Safety Tip because the domain looked like another domain. */ possibleSpoofingUrl: 'Possible spoofing URL', /** *@description Body of a warning when the user visits a page that triggered a Safety Tip because the domain looked like another domain. *@example {wikipedia.org} PH1 */ thisSitesHostnameLooksSimilarToP: 'This site\'s hostname looks similar to {PH1}. Attackers sometimes mimic sites by making small, hard-to-see changes to the domain name.', /** *@description second part of body of a warning when the user visits a page that triggered a Safety Tip because the domain looked like another domain. */ ifYouBelieveThisIsShownInErrorSafety: 'If you believe this is shown in error please visit https://g.co/chrome/lookalike-warnings.', /** *@description Title of the devtools security tab when the page you are on triggered a safety tip. */ thisPageIsSuspiciousFlaggedBy: 'This page is suspicious (flagged by Chrome).', /** *@description Text for a security certificate */ certificate: 'Certificate', /** *@description Summary phrase for a security problem where the site's certificate chain contains a SHA1 signature. */ insecureSha: 'insecure (SHA-1)', /** *@description Description of a security problem where the site's certificate chain contains a SHA1 signature. */ theCertificateChainForThisSite: 'The certificate chain for this site contains a certificate signed using SHA-1.', /** *@description Summary phrase for a security problem where the site's certificate is missing a subjectAltName extension. */ subjectAlternativeNameMissing: '`Subject Alternative Name` missing', /** *@description Description of a security problem where the site's certificate is missing a subjectAltName extension. */ theCertificateForThisSiteDoesNot: 'The certificate for this site does not contain a `Subject Alternative Name` extension containing a domain name or IP address.', /** *@description Summary phrase for a security problem with the site's certificate. */ missing: 'missing', /** *@description Description of a security problem with the site's certificate. *@example {net::ERR_CERT_AUTHORITY_INVALID} PH1 */ thisSiteIsMissingAValidTrusted: 'This site is missing a valid, trusted certificate ({PH1}).', /** *@description Summary phrase for a site that has a valid server certificate. */ validAndTrusted: 'valid and trusted', /** *@description Description of a site that has a valid server certificate. *@example {Let's Encrypt Authority X3} PH1 */ theConnectionToThisSiteIsUsingA: 'The connection to this site is using a valid, trusted server certificate issued by {PH1}.', /** *@description Summary phrase for a security state where Private Key Pinning is ignored because the certificate chains to a locally-trusted root. */ publickeypinningBypassed: 'Public-Key-Pinning bypassed', /** *@description Description of a security state where Private Key Pinning is ignored because the certificate chains to a locally-trusted root. */ publickeypinningWasBypassedByA: 'Public-Key-Pinning was bypassed by a local root certificate.', /** *@description Summary phrase for a site with a certificate that is expiring soon. */ certificateExpiresSoon: 'Certificate expires soon', /** *@description Description for a site with a certificate that is expiring soon. */ theCertificateForThisSiteExpires: 'The certificate for this site expires in less than 48 hours and needs to be renewed.', /** *@description Text that refers to the network connection */ connection: 'Connection', /** *@description Summary phrase for a site that uses a modern, secure TLS protocol and cipher. */ secureConnectionSettings: 'secure connection settings', /** *@description Description of a site's TLS settings. *@example {TLS 1.2} PH1 *@example {ECDHE_RSA} PH2 *@example {AES_128_GCM} PH3 */ theConnectionToThisSiteIs: 'The connection to this site is encrypted and authenticated using {PH1}, {PH2}, and {PH3}.', /** *@description A recommendation to the site owner to use a modern TLS protocol *@example {TLS 1.0} PH1 */ sIsObsoleteEnableTlsOrLater: '{PH1} is obsolete. Enable TLS 1.2 or later.', /** *@description A recommendation to the site owner to use a modern TLS key exchange */ rsaKeyExchangeIsObsoleteEnableAn: 'RSA key exchange is obsolete. Enable an ECDHE-based cipher suite.', /** *@description A recommendation to the site owner to use a modern TLS cipher *@example {3DES_EDE_CBC} PH1 */ sIsObsoleteEnableAnAesgcmbased: '{PH1} is obsolete. Enable an AES-GCM-based cipher suite.', /** *@description A recommendation to the site owner to use a modern TLS server signature */ theServerSignatureUsesShaWhichIs: 'The server signature uses SHA-1, which is obsolete. Enable a SHA-2 signature algorithm instead. (Note this is different from the signature in the certificate.)', /** *@description Summary phrase for a site that uses an outdated SSL settings (protocol, key exchange, or cipher). */ obsoleteConnectionSettings: 'obsolete connection settings', /** *@description A title of the 'Resources' action category */ resources: 'Resources', /** *@description Summary for page when there is active mixed content */ activeMixedContent: 'active mixed content', /** *@description Description for page when there is active mixed content */ youHaveRecentlyAllowedNonsecure: 'You have recently allowed non-secure content (such as scripts or iframes) to run on this site.', /** *@description Summary for page when there is mixed content */ mixedContent: 'mixed content', /** *@description Description for page when there is mixed content */ thisPageIncludesHttpResources: 'This page includes HTTP resources.', /** *@description Summary for page when there is a non-secure form */ nonsecureForm: 'non-secure form', /** *@description Description for page when there is a non-secure form */ thisPageIncludesAFormWithA: 'This page includes a form with a non-secure "action" attribute.', /** *@description Summary for the page when it contains active content with certificate error */ activeContentWithCertificate: 'active content with certificate errors', /** *@description Description for the page when it contains active content with certificate error */ youHaveRecentlyAllowedContent: 'You have recently allowed content loaded with certificate errors (such as scripts or iframes) to run on this site.', /** *@description Summary for page when there is active content with certificate errors */ contentWithCertificateErrors: 'content with certificate errors', /** *@description Description for page when there is content with certificate errors */ thisPageIncludesResourcesThat: 'This page includes resources that were loaded with certificate errors.', /** *@description Summary for page when all resources are served securely */ allServedSecurely: 'all served securely', /** *@description Description for page when all resources are served securely */ allResourcesOnThisPageAreServed: 'All resources on this page are served securely.', /** *@description Text in Security Panel of the Security panel */ blockedMixedContent: 'Blocked mixed content', /** *@description Text in Security Panel of the Security panel */ yourPageRequestedNonsecure: 'Your page requested non-secure resources that were blocked.', /** *@description Refresh prompt text content in Security Panel of the Security panel */ reloadThePageToRecordRequestsFor: 'Reload the page to record requests for HTTP resources.', /** * @description Link text in the Security Panel. Clicking the link navigates the user to the * Network panel. Requests refers to network requests. Each request is a piece of data transmitted * from the current user's browser to a remote server. */ viewDRequestsInNetworkPanel: '{n, plural, =1 {View # request in Network Panel} other {View # requests in Network Panel}}', /** *@description Text for the origin of something */ origin: 'Origin', /** *@description Text in Security Panel of the Security panel */ viewRequestsInNetworkPanel: 'View requests in Network Panel', /** *@description Text for security or network protocol */ protocol: 'Protocol', /** *@description Text in the Security panel that refers to how the TLS handshake *established encryption keys. */ keyExchange: 'Key exchange', /** *@description Text in Security Panel that refers to how the TLS handshake *encrypted data. */ cipher: 'Cipher', /** *@description Text in Security Panel that refers to the signature algorithm *used by the server for authenticate in the TLS handshake. */ serverSignature: 'Server signature', /** *@description Text in Security Panel that refers to whether the ClientHello *message in the TLS handshake was encrypted. */ encryptedClientHello: 'Encrypted ClientHello', /** *@description Sct div text content in Security Panel of the Security panel */ certificateTransparency: 'Certificate Transparency', /** *@description Text that refers to the subject of a security certificate */ subject: 'Subject', /** *@description Text to show since when an item is valid */ validFrom: 'Valid from', /** *@description Text to indicate the expiry date */ validUntil: 'Valid until', /** *@description Text for the issuer of an item */ issuer: 'Issuer', /** *@description Text in Security Panel of the Security panel */ openFullCertificateDetails: 'Open full certificate details', /** *@description Text in Security Panel of the Security panel */ sct: 'SCT', /** *@description Text in Security Panel of the Security panel */ logName: 'Log name', /** *@description Text in Security Panel of the Security panel */ logId: 'Log ID', /** *@description Text in Security Panel of the Security panel */ validationStatus: 'Validation status', /** *@description Text for the source of something */ source: 'Source', /** * @description Label for a date/time string in the Security panel. It indicates the time at which * a security certificate was issued (created by an authority and distributed). */ issuedAt: 'Issued at', /** *@description Text in Security Panel of the Security panel */ hashAlgorithm: 'Hash algorithm', /** *@description Text in Security Panel of the Security panel */ signatureAlgorithm: 'Signature algorithm', /** *@description Text in Security Panel of the Security panel */ signatureData: 'Signature data', /** *@description Toggle scts details link text content in Security Panel of the Security panel */ showFullDetails: 'Show full details', /** *@description Toggle scts details link text content in Security Panel of the Security panel */ hideFullDetails: 'Hide full details', /** *@description Text in Security Panel of the Security panel */ thisRequestCompliesWithChromes: 'This request complies with `Chrome`\'s Certificate Transparency policy.', /** *@description Text in Security Panel of the Security panel */ thisRequestDoesNotComplyWith: 'This request does not comply with `Chrome`\'s Certificate Transparency policy.', /** *@description Text in Security Panel of the Security panel */ thisResponseWasLoadedFromCache: 'This response was loaded from cache. Some security details might be missing.', /** *@description Text in Security Panel of the Security panel */ theSecurityDetailsAboveAreFrom: 'The security details above are from the first inspected response.', /** *@description Main summary for where the site has a non-cryptographic secure origin. */ thisOriginIsANonhttpsSecure: 'This origin is a non-HTTPS secure origin.', /** *@description Text in Security Panel of the Security panel */ yourConnectionToThisOriginIsNot: 'Your connection to this origin is not secure.', /** *@description No info div text content in Security Panel of the Security panel */ noSecurityInformation: 'No security information', /** *@description Text in Security Panel of the Security panel */ noSecurityDetailsAreAvailableFor: 'No security details are available for this origin.', /** *@description San div text content in Security Panel of the Security panel */ na: '(n/a)', /** *@description Text to show less content */ showLess: 'Show less', /** *@description Truncated santoggle text content in Security Panel of the Security panel *@example {2} PH1 */ showMoreSTotal: 'Show more ({PH1} total)', /** *@description Shown when a field refers to an option that is unknown to the frontend. */ unknownField: 'unknown', /** *@description Shown when a field refers to a TLS feature which was enabled. */ enabled: 'enabled', } as const; const str_ = i18n.i18n.registerUIStrings('panels/security/SecurityPanel.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); let securityPanelInstance: SecurityPanel; // See https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-signaturescheme // This contains signature schemes supported by Chrome. const SignatureSchemeStrings = new Map([ // The full name for these schemes is RSASSA-PKCS1-v1_5, sometimes // "PKCS#1 v1.5", but those are very long, so let "RSA" vs "RSA-PSS" // disambiguate. [0x0201, 'RSA with SHA-1'], [0x0401, 'RSA with SHA-256'], [0x0501, 'RSA with SHA-384'], [0x0601, 'RSA with SHA-512'], // We omit the curve from these names because in TLS 1.2 these code points // were not specific to a curve. Saying "P-256" for a server that used a P-384 // key with SHA-256 in TLS 1.2 would be confusing. [0x0403, 'ECDSA with SHA-256'], [0x0503, 'ECDSA with SHA-384'], [0x0804, 'RSA-PSS with SHA-256'], [0x0805, 'RSA-PSS with SHA-384'], [0x0806, 'RSA-PSS with SHA-512'], ]); const LOCK_ICON_NAME = 'lock'; const WARNING_ICON_NAME = 'warning'; const UNKNOWN_ICON_NAME = 'indeterminate-question-box'; export function getSecurityStateIconForDetailedView( securityState: Protocol.Security.SecurityState, className: string): IconButton.Icon.Icon { let iconName: string; switch (securityState) { case Protocol.Security.SecurityState.Neutral: // fallthrough case Protocol.Security.SecurityState.Insecure: // fallthrough case Protocol.Security.SecurityState.InsecureBroken: iconName = WARNING_ICON_NAME; break; case Protocol.Security.SecurityState.Secure: iconName = LOCK_ICON_NAME; break; case Protocol.Security.SecurityState.Info: // fallthrough case Protocol.Security.SecurityState.Unknown: iconName = UNKNOWN_ICON_NAME; break; } return IconButton.Icon.create(iconName, className); } export function getSecurityStateIconForOverview( securityState: Protocol.Security.SecurityState, className: string): IconButton.Icon.Icon { let iconName: string; switch (securityState) { case Protocol.Security.SecurityState.Unknown: // fallthrough case Protocol.Security.SecurityState.Neutral: iconName = UNKNOWN_ICON_NAME; break; case Protocol.Security.SecurityState.Insecure: // fallthrough case Protocol.Security.SecurityState.InsecureBroken: iconName = WARNING_ICON_NAME; break; case Protocol.Security.SecurityState.Secure: iconName = LOCK_ICON_NAME; break; case Protocol.Security.SecurityState.Info: throw new Error(`Unexpected security state ${securityState}`); } return IconButton.Icon.create(iconName, className); } export function createHighlightedUrl(url: Platform.DevToolsPath.UrlString, securityState: string): Element { const schemeSeparator = '://'; const index = url.indexOf(schemeSeparator); // If the separator is not found, just display the text without highlighting. if (index === -1) { const text = document.createElement('span'); text.textContent = url; return text; } const highlightedUrl = document.createElement('span'); highlightedUrl.classList.add('highlighted-url'); const scheme = url.substr(0, index); const content = url.substr(index + schemeSeparator.length); highlightedUrl.createChild('span', 'url-scheme-' + securityState).textContent = scheme; highlightedUrl.createChild('span', 'url-scheme-separator').textContent = schemeSeparator; highlightedUrl.createChild('span').textContent = content; return highlightedUrl; } export interface ViewInput { panel: SecurityPanel; } export interface ViewOutput { setVisibleView: (view: UI.Widget.VBox) => void; splitWidget: UI.SplitWidget.SplitWidget; mainView: SecurityMainView; visibleView: UI.Widget.VBox|null; sidebar: SecurityPanelSidebar; } export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void; export class SecurityPanel extends UI.Panel.Panel implements SDK.TargetManager.SDKModelObserver<SecurityModel> { readonly mainView: SecurityMainView; readonly sidebar!: SecurityPanelSidebar; private readonly lastResponseReceivedForLoaderId: Map<string, SDK.NetworkRequest.NetworkRequest>; private readonly origins: Map<string, OriginState>; private readonly filterRequestCounts: Map<string, number>; visibleView: UI.Widget.VBox|null; private eventListeners: Common.EventTarget.EventDescriptor[]; private securityModel: SecurityModel|null; readonly splitWidget!: UI.SplitWidget.SplitWidget; constructor(private view: View = (_input, output, target) => { // clang-format off render( html` <devtools-split-view direction="column" name="security" ${UI.Widget.widgetRef(UI.SplitWidget.SplitWidget, e => {output.splitWidget = e;})}> <devtools-widget slot="sidebar" .widgetConfig=${widgetConfig(SecurityPanelSidebar)} @showCookieReport=${()=>output.setVisibleView(new CookieReportView())} @showFlagControls=${() => output.setVisibleView(new CookieControlsView())} ${UI.Widget.widgetRef(SecurityPanelSidebar, e => {output.sidebar = e;})}> </devtools-widget> </devtools-split-view>`, target, {host: this}); // clang-format on }) { super('security'); this.update(); this.sidebar.setMinimumSize(100, 25); this.sidebar.element.classList.add('panel-sidebar'); this.sidebar.element.setAttribute('jslog', `${VisualLogging.pane('sidebar').track({resize: true})}`); this.mainView = new SecurityMainView(); this.mainView.panel = this; this.element.addEventListener(ShowOriginEvent.eventName, (event: ShowOriginEvent) => { if (event.origin) { this.showOrigin(event.origin); } else { this.setVisibleView(this.mainView); } }); this.lastResponseReceivedForLoaderId = new Map(); this.origins = new Map(); this.filterRequestCounts = new Map(); this.visibleView = null; this.eventListeners = []; this.securityModel = null; SDK.TargetManager.TargetManager.instance().observeModels(SecurityModel, this, {scoped: true}); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.PrimaryPageChanged, this.onPrimaryPageChanged, this); this.sidebar.showLastSelectedElement(); } static instance(opts: {forceNew: boolean|null} = {forceNew: null}): SecurityPanel { const {forceNew} = opts; if (!securityPanelInstance || forceNew) { securityPanelInstance = new SecurityPanel(); } return securityPanelInstance; } static createCertificateViewerButtonForOrigin(text: string, origin: string): Element { const certificateButton = UI.UIUtils.createTextButton(text, async (e: Event) => { e.consume(); const names = await SDK.NetworkManager.MultitargetNetworkManager.instance().getCertificate(origin); if (names.length > 0) { Host.InspectorFrontendHost.InspectorFrontendHostInstance.showCertificateViewer(names); } }, {className: 'origin-button', jslogContext: 'security.view-certificate-for-origin', title: text}); UI.ARIAUtils.markAsButton(certificateButton); return certificateButton; } static createCertificateViewerButtonForCert(text: string, names: string[]): Element { const certificateButton = UI.UIUtils.createTextButton(text, e => { e.consume(); Host.InspectorFrontendHost.InspectorFrontendHostInstance.showCertificateViewer(names); }, {className: 'origin-button', jslogContext: 'security.view-certificate'}); UI.ARIAUtils.markAsButton(certificateButton); return certificateButton; } update(): void { this.view({panel: this}, this, this.contentElement); } private updateVisibleSecurityState(visibleSecurityState: PageVisibleSecurityState): void { this.sidebar.securityOverviewElement.setSecurityState(visibleSecurityState.securityState); this.mainView.updateVisibleSecurityState(visibleSecurityState); } private onVisibleSecurityStateChanged({data}: Common.EventTarget.EventTargetEvent<PageVisibleSecurityState>): void { this.updateVisibleSecurityState(data); } showOrigin(origin: Platform.DevToolsPath.UrlString): void { const originState = this.origins.get(origin); if (!originState) { return; } if (!originState.originView) { originState.originView = new SecurityOriginView(origin, originState); } this.setVisibleView(originState.originView); } override wasShown(): void { super.wasShown(); if (!this.visibleView) { this.sidebar.showLastSelectedElement(); } } override focus(): void { this.sidebar.focus(); } setVisibleView(view: UI.Widget.VBox): void { if (this.visibleView === view) { return; } if (this.visibleView) { this.visibleView.detach(); } this.visibleView = view; if (view) { this.splitWidget.setMainWidget(view); } } private onResponseReceived(event: Common.EventTarget.EventTargetEvent<SDK.NetworkManager.ResponseReceivedEvent>): void { const request = event.data.request; if (request.resourceType() === Common.ResourceType.resourceTypes.Document && request.loaderId) { this.lastResponseReceivedForLoaderId.set(request.loaderId, request); } } private processRequest(request: SDK.NetworkRequest.NetworkRequest): void { const origin = Common.ParsedURL.ParsedURL.extractOrigin(request.url()); if (!origin) { // We don't handle resources like data: URIs. Most of them don't affect the lock icon. return; } let securityState = request.securityState(); if (request.mixedContentType === Protocol.Security.MixedContentType.Blockable || request.mixedContentType === Protocol.Security.MixedContentType.OptionallyBlockable) { securityState = Protocol.Security.SecurityState.Insecure; } const originState = this.origins.get(origin); if (originState) { if (securityStateCompare(securityState, originState.securityState) < 0) { originState.securityState = securityState; const securityDetails = request.securityDetails(); if (securityDetails) { originState.securityDetails = securityDetails; } this.sidebar.updateOrigin(origin, securityState); if (originState.originView) { originState.originView.setSecurityState(securityState); } } } else { // This stores the first security details we see for an origin, but we should // eventually store a (deduplicated) list of all the different security // details we have seen. https://crbug.com/503170 const newOriginState: OriginState = { securityState, securityDetails: request.securityDetails(), loadedFromCache: request.cached(), originView: undefined, }; this.origins.set(origin, newOriginState); this.sidebar.addOrigin(origin, securityState); // Don't construct the origin view yet (let it happen lazily). } } private onRequestFinished(event: Common.EventTarget.EventTargetEvent<SDK.NetworkRequest.NetworkRequest>): void { const request = event.data; this.updateFilterRequestCounts(request); this.processRequest(request); } private updateFilterRequestCounts(request: SDK.NetworkRequest.NetworkRequest): void { if (request.mixedContentType === Protocol.Security.MixedContentType.None) { return; } let filterKey: string = NetworkForward.UIFilter.MixedContentFilterValues.ALL; if (request.wasBlocked()) { filterKey = NetworkForward.UIFilter.MixedContentFilterValues.BLOCKED; } else if (request.mixedContentType === Protocol.Security.MixedContentType.Blockable) { filterKey = NetworkForward.UIFilter.MixedContentFilterValues.BLOCK_OVERRIDDEN; } else if (request.mixedContentType === Protocol.Security.MixedContentType.OptionallyBlockable) { filterKey = NetworkForward.UIFilter.MixedContentFilterValues.DISPLAYED; } const currentCount = this.filterRequestCounts.get(filterKey); if (!currentCount) { this.filterRequestCounts.set(filterKey, 1); } else { this.filterRequestCounts.set(filterKey, currentCount + 1); } this.mainView.refreshExplanations(); } filterRequestCount(filterKey: string): number { return this.filterRequestCounts.get(filterKey) || 0; } modelAdded(securityModel: SecurityModel): void { if (securityModel.target() !== securityModel.target().outermostTarget()) { return; } this.securityModel = securityModel; const resourceTreeModel = securityModel.resourceTreeModel(); const networkManager = securityModel.networkManager(); if (this.eventListeners.length) { Common.EventTarget.removeEventListeners(this.eventListeners); } this.eventListeners = [ securityModel.addEventListener(Events.VisibleSecurityStateChanged, this.onVisibleSecurityStateChanged, this), resourceTreeModel.addEventListener( SDK.ResourceTreeModel.Events.InterstitialShown, this.onInterstitialShown, this), resourceTreeModel.addEventListener( SDK.ResourceTreeModel.Events.InterstitialHidden, this.onInterstitialHidden, this), networkManager.addEventListener(SDK.NetworkManager.Events.ResponseReceived, this.onResponseReceived, this), networkManager.addEventListener(SDK.NetworkManager.Events.RequestFinished, this.onRequestFinished, this), ]; if (resourceTreeModel.isInterstitialShowing) { this.onInterstitialShown(); } } modelRemoved(securityModel: SecurityModel): void { if (this.securityModel !== securityModel) { return; } this.securityModel = null; Common.EventTarget.removeEventListeners(this.eventListeners); } private onPrimaryPageChanged( event: Common.EventTarget.EventTargetEvent< {frame: SDK.ResourceTreeModel.ResourceTreeFrame, type: SDK.ResourceTreeModel.PrimaryPageChangeType}>): void { const {frame} = event.data; const request = this.lastResponseReceivedForLoaderId.get(frame.loaderId); this.sidebar.showLastSelectedElement(); this.sidebar.clearOrigins(); this.origins.clear(); this.lastResponseReceivedForLoaderId.clear(); this.filterRequestCounts.clear(); // After clearing the filtered request counts, refresh the // explanations to reflect the new counts. this.mainView.refreshExplanations(); // If we could not find a matching request (as in the case of clicking // through an interstitial, see https://crbug.com/669309), set the origin // based upon the url data from the PrimaryPageChanged event itself. const origin = Common.ParsedURL.ParsedURL.extractOrigin(request ? request.url() : frame.url); this.sidebar.setMainOrigin(origin); if (request) { this.processRequest(request); } } private onInterstitialShown(): void { // The panel might have been displaying the origin view on the // previously loaded page. When showing an interstitial, switch // back to the sidebar's last shown view. this.sidebar.showLastSelectedElement(); this.sidebar.toggleOriginsList(true /* hidden */); } private onInterstitialHidden(): void { this.sidebar.toggleOriginsList(false /* hidden */); } } export enum OriginGroup { /* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */ MainOrigin = 'MainOrigin', NonSecure = 'NonSecure', Secure = 'Secure', Unknown = 'Unknown', /* eslint-enable @typescript-eslint/naming-convention */ } export class SecurityMainView extends UI.Widget.VBox { panel!: SecurityPanel; private readonly summarySection: HTMLElement; private readonly securityExplanationsMain: HTMLElement; private readonly securityExplanationsExtra: HTMLElement; private readonly lockSpectrum: Map<Protocol.Security.SecurityState, HTMLElement>; private summaryText: HTMLElement; private explanations: Array<Protocol.Security.SecurityStateExplanation|SecurityStyleExplanation>|null; private securityState: Protocol.Security.SecurityState|null; constructor(element?: HTMLElement) { super(undefined, undefined, element); this.registerRequiredCSS(lockIconStyles, mainViewStyles); this.element.setAttribute('jslog', `${VisualLogging.pane('security.main-view')}`); this.setMinimumSize(200, 100); this.contentElement.classList.add('security-main-view'); this.summarySection = this.contentElement.createChild('div', 'security-summary'); // Info explanations should appear after all others. this.securityExplanationsMain = this.contentElement.createChild('div', 'security-explanation-list security-explanations-main'); this.securityExplanationsExtra = this.contentElement.createChild('div', 'security-explanation-list security-explanations-extra'); // Fill the security summary section. const summaryDiv = this.summarySection.createChild('div', 'security-summary-section-title'); summaryDiv.textContent = i18nString(UIStrings.securityOverview); UI.ARIAUtils.markAsHeading(summaryDiv, 1); const lockSpectrum = this.summarySection.createChild('div', 'lock-spectrum'); this.lockSpectrum = new Map([ [ Protocol.Security.SecurityState.Secure, lockSpectrum.appendChild( getSecurityStateIconForOverview(Protocol.Security.SecurityState.Secure, 'lock-icon lock-icon-secure')), ], [ Protocol.Security.SecurityState.Neutral, lockSpectrum.appendChild( getSecurityStateIconForOverview(Protocol.Security.SecurityState.Neutral, 'lock-icon lock-icon-neutral')), ], [ Protocol.Security.SecurityState.Insecure, lockSpectrum.appendChild( getSecurityStateIconForOverview(Protocol.Security.SecurityState.Insecure, 'lock-icon lock-icon-insecure')), ], ]); UI.Tooltip.Tooltip.install( this.getLockSpectrumDiv(Protocol.Security.SecurityState.Secure), i18nString(UIStrings.secure)); UI.Tooltip.Tooltip.install( this.getLockSpectrumDiv(Protocol.Security.SecurityState.Neutral), i18nString(UIStrings.info)); UI.Tooltip.Tooltip.install( this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure), i18nString(UIStrings.notSecure)); this.summarySection.createChild('div', 'triangle-pointer-container') .createChild('div', 'triangle-pointer-wrapper') .createChild('div', 'triangle-pointer'); this.summaryText = this.summarySection.createChild('div', 'security-summary-text'); UI.ARIAUtils.markAsHeading(this.summaryText, 2); this.explanations = null; this.securityState = null; } getLockSpectrumDiv(securityState: Protocol.Security.SecurityState): HTMLElement { const element = this.lockSpectrum.get(securityState); if (!element) { throw new Error(`Invalid argument: ${securityState}`); } return element; } private addExplanation( parent: Element, explanation: Protocol.Security.SecurityStateExplanation|SecurityStyleExplanation): Element { const explanationSection = parent.createChild('div', 'security-explanation'); explanationSection.classList.add('security-explanation-' + explanation.securityState); const icon = getSecurityStateIconForDetailedView( explanation.securityState, 'security-property security-property-' + explanation.securityState); explanationSection.appendChild(icon); const text = explanationSection.createChild('div', 'security-explanation-text'); const explanationHeader = text.createChild('div', 'security-explanation-title'); if (explanation.title) { explanationHeader.createChild('span').textContent = explanation.title + ' - '; explanationHeader.createChild('span', 'security-explanation-title-' + explanation.securityState).textContent = explanation.summary; } else { explanationHeader.textContent = explanation.summary; } text.createChild('div').textContent = explanation.description; if (explanation.certificate.length) { text.appendChild(SecurityPanel.createCertificateViewerButtonForCert( i18nString(UIStrings.viewCertificate), explanation.certificate)); } if (explanation.recommendations?.length) { const recommendationList = text.createChild('ul', 'security-explanation-recommendations'); for (const recommendation of explanation.recommendations) { recommendationList.createChild('li').textContent = recommendation; } } return text; } updateVisibleSecurityState(visibleSecurityState: PageVisibleSecurityState): void { // Remove old state. // It's safe to call this even when this.securityState is undefined. this.summarySection.classList.remove('security-summary-' + this.securityState); // Add new state. this.securityState = visibleSecurityState.securityState; this.summarySection.classList.add('security-summary-' + this.securityState); // Update the color and title of the triangle icon in the lock spectrum to // match the security state. if (this.securityState === Protocol.Security.SecurityState.Insecure) { this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure).classList.add('lock-icon-insecure'); this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure).classList.remove('lock-icon-insecure-broken'); UI.Tooltip.Tooltip.install( this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure), i18nString(UIStrings.notSecure)); } else if (this.securityState === Protocol.Security.SecurityState.InsecureBroken) { this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure).classList.add('lock-icon-insecure-broken'); this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure).classList.remove('lock-icon-insecure'); UI.Tooltip.Tooltip.install( this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure), i18nString(UIStrings.notSecureBroken)); } const {summary, explanations} = this.getSecuritySummaryAndExplanations(visibleSecurityState); // Use override summary if present, otherwise use base explanation this.summaryText.textContent = summary || SummaryMessages[this.securityState](); this.explanations = this.orderExplanations(explanations); this.refreshExplanations(); } private getSecuritySummaryAndExplanations(visibleSecurityState: PageVisibleSecurityState): {summary: (string|undefined), explanations: SecurityStyleExplanation[]} { const {securityState, securityStateIssueIds} = visibleSecurityState; let summary; const explanations: SecurityStyleExplanation[] = []; summary = this.explainSafetyTipSecurity(visibleSecurityState, summary, explanations); if (securityStateIssueIds.includes('malicious-content')) { summary = i18nString(UIStrings.thisPageIsDangerousFlaggedBy); // Always insert SafeBrowsing explanation at the front. explanations.unshift(new SecurityStyleExplanation( Protocol.Security.SecurityState.Insecure, undefined, i18nString(UIStrings.flaggedByGoogleSafeBrowsing), i18nString(UIStrings.toCheckThisPagesStatusVisit))); } else if ( securityStateIssueIds.includes('is-error-page') && (visibleSecurityState.certificateSecurityState?.certificateNetworkError === null)) { summary = i18nString(UIStrings.thisIsAnErrorPage); // In the case of a non cert error page, we usually don't have a // certificate, connection, or content that needs to be explained, e.g. in // the case of a net error, so we can early return. return {summary, explanations}; } else if ( securityState === Protocol.Security.SecurityState.InsecureBroken && securityStateIssueIds.includes('scheme-is-not-cryptographic')) { summary = summary || i18nString(UIStrings.thisPageIsInsecureUnencrypted); } if (securityStateIssueIds.includes('scheme-is-not-cryptographic')) { if (securityState === Protocol.Security.SecurityState.Neutral && !securityStateIssueIds.includes('insecure-origin')) { summary = i18nString(UIStrings.thisPageHasANonhttpsSecureOrigin); } return {summary, explanations}; } this.explainCertificateSecurity(visibleSecurityState, explanations); this.explainConnectionSecurity(visibleSecurityState, explanations); this.explainContentSecurity(visibleSecurityState, explanations); return {summary, explanations}; } private explainSafetyTipSecurity( visibleSecurityState: PageVisibleSecurityState, summary: string|undefined, explanations: SecurityStyleExplanation[]): string|undefined { const {securityStateIssueIds, safetyTipInfo} = visibleSecurityState; const currentExplanations = []; if (securityStateIssueIds.includes('bad_reputation')) { const formatedDescription = `${i18nString(UIStrings.chromeHasDeterminedThatThisSiteS)}\n\n${ i18nString(UIStrings.ifYouBelieveThisIsShownIn)}`; currentExplanations.push({ summary: i18nString(UIStrings.thisPageIsSuspicious), description: formatedDescription, }); } else if (securityStateIssueIds.includes('lookalike') && safetyTipInfo?.safeUrl) { const hostname = new URL(safetyTipInfo.safeUrl).hostname; const hostnamePlaceholder = {PH1: hostname}; const formatedDescriptionSafety = `${i18nString(UIStrings.thisSitesHostnameLooksSimilarToP, hostnamePlaceholder)}\n\n${ i18nString(UIStrings.ifYouBelieveThisIsShownInErrorSafety)}`; currentExplanations.push( {summary: i18nString(UIStrings.possibleSpoofingUrl), description: formatedDescriptionSafety}); } if (currentExplanations.length > 0) { // To avoid overwriting SafeBrowsing's title, set the main summary only if // it's empty. The title set here can be overridden by later checks (e.g. // bad HTTP). summary = summary || i18nString(UIStrings.thisPageIsSuspiciousFlaggedBy); explanations.push(new SecurityStyleExplanation( Protocol.Security.SecurityState.Insecure, undefined, currentExplanations[0].summary, currentExplanations[0].description)); } return summary; } private explainCertificateSecurity( visibleSecurityState: PageVisibleSecurityState, explanations: SecurityStyleExplanation[]): void { const {certificateSecurityState, securityStateIssueIds} = visibleSecurityState; const title = i18nString(UIStrings.certificate); if (certificateSecurityState?.certificateHasSha1Signature) { const explanationSummary = i18nString(UIStrings.insecureSha); const description = i18nString(UIStrings.theCertificateChainForThisSite); if (certificateSecurityState.certificateHasWeakSignature) { explanations.push(new SecurityStyleExplanation( Protocol.Security.SecurityState.Insecure, title, explanationSummary, description, certificateSecurityState.certificate, Protocol.Security.MixedContentType.None)); } else { explanations.push(new SecurityStyleExplanation( Protocol.Security.SecurityState.Neutral, title, explanationSummary, description, certificateSecurityState.certificate, Protocol.Security.MixedContentType.None)); } } if (certificateSecurityState && securityStateIssueIds.includes('cert-missing-subject-alt-name')) { explanations.push(new SecurityStyleExplanation( Protocol.Security.SecurityState.Insecure, title, i18nString(UIStrings.subjectAlternativeNameMissing), i18nString(UIStrings.theCertificateForThisSiteDoesNot), certificateSecurityState.certificate, Protocol.Security.MixedContentType.None)); } if (certificateSecurityState && certificateSecurityState.certificateNetworkError !== null) { explanations.push(new SecurityStyleExplanation( Protocol.Security.SecurityState.Insecure, title, i18nString(UIStrings.missing), i18nString(UIStrings.thisSiteIsMissingAValidTrusted, {PH1: certificateSecurityState.certificateNetworkError}), certificateSecurityState.certificate, Protocol.Security.MixedContentType.None)); } else if (certificateSecurityState && !certificateSecurityState.certificateHasSha1Signature) { explanations.push(new SecurityStyleExplanation( Protocol.Security.SecurityState.Secure, title, i18nString(UIStrings.validAndTrusted), i18nString(UIStrings.theConnectionToThisSiteIsUsingA, {PH1: certificateSecurityState.issuer}), certificateSecurityState.certificate, Protocol.Security.MixedContentType.None)); } if (securityStateIssueIds.includes('pkp-bypassed')) { explanations.push(new SecurityStyleExplanation( Protocol.Security.SecurityState.Info, title, i18nString(UIStrings.publickeypinningBypassed), i18nString(UIStrings.publickeypinningWasBypassedByA))); } if (certificateSecurityState?.isCertificateExpiringSoon()) { explanations.push(new SecurityStyleExplanation( Protocol.Security.SecurityState.Info, undefined, i18nString(UIStrings.certificateExpiresSoon), i18nString(UIStrings.theCertificateForThisSiteExpires))); } } private explainConnectionSecurity( visibleSecurityState: PageVisibleSecurityState, explanations: SecurityStyleExplanation[]): void { const certificateSecurityState = visibleSecurityState.certificateSecurityState; if (!certificateSecurityState) { return; } const title = i18nString(UIStrings.connection); if (certificateSecurityState.modernSSL) { explanations.push(new SecurityStyleExplanation( Protocol.Security.SecurityState.Secure, title, i18nString(UIStrings.secureConnectionSettings), i18nString(UIStrings.theConnectionToThisSiteIs, { PH1: certificateSecurityState.protocol, PH2: certificateSecurityState.getKeyExchangeName(), PH3: certificateSecurityState.getCipherFullName(), }))); return; } const recommendations = []; if (certificateSecurityState.obsoleteSslProtocol) { recommendations.push(i18nString(UIStrings.sIsObsoleteEnableTlsOrLater, {PH1: certificateSecurityState.protocol})); } if (certificateSecurityState.obsoleteSslKeyExchange) { recommendations.push(i18nString(UIStrings.rsaKeyExchangeIsObsoleteEnableAn)); } if (certificateSecurityState.obsoleteSslCipher) { recommendations.push( i18nString(UIStrings.sIsObsoleteEnableAnAesgcmbased, {PH1: certificateSecurityState.cipher})); } if (certificateSecurityState.obsoleteSslSignature) { recommendations.push(i18nString(UIStrings.theServerSignatureUsesShaWhichIs)); } explanations.push(new SecurityStyleExplanation( Protocol.Security.SecurityState.Info, title, i18nString(UIStrings.obsoleteConnectionSettings), i18nString(UIStrings.theConnectionToThisSiteIs, { PH1: certificateSecurityState.protocol, PH2: certificateSecurityState.getKeyExchangeName(), PH3: certificateSecurityState.getCipherFullName(), }), undefined, undefined, recommendations)); } private explainContentSecurity( visibleSecurityState: PageVisibleSecurityState, explanations: SecurityStyleExplanation[]): void { // Add the secure explanation unless there is an issue. let addSecureExplanation = true; const title = i18nString(UIStrings.resources); const securityStateIssueIds = visibleSecurityState.securityStateIssueIds; if (securityStateIssueIds.includes('ran-mixed-content')) { addSecureExplanation = false; explanations.push(new SecurityStyleExplanation( Protocol.Security.SecurityState.Insecure, title, i18nString(UIStrings.activeMixedContent), i18nString(UIStrings.youHaveRecentlyAllowedNonsecure), [], Protocol.Security.MixedContentType.Blockable)); } if (securityStateIssueIds.includes('displayed-mixed-content')) { addSecureExplanation = false; explanations.push(new SecurityStyleExplanation( Protocol.Security.SecurityState.Neutral, title, i18nString(UIStrings.mixedContent), i18nString(UIStrings.thisPageIncludesHttpResources), [], Protocol.Security.MixedContentType.OptionallyBlockable)); } if (securityStateIssueIds.includes('contained-mixed-form')) { addSecureExplanation = false; explanations.push(new Security