diff --git a/packages/extension-base/src/background/handlers/Tabs.ts b/packages/extension-base/src/background/handlers/Tabs.ts index d984aa85..ef387795 100644 --- a/packages/extension-base/src/background/handlers/Tabs.ts +++ b/packages/extension-base/src/background/handlers/Tabs.ts @@ -8,6 +8,7 @@ import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types'; import { SubjectInfo } from '@polkadot/ui-keyring/observable/types'; import { RequestAuthorizeTab, ResponseSigning, RequestTypes, ResponseTypes, MessageTypes, ResponseRpcListProviders, RequestRpcSend, RequestRpcSubscribe, RequestRpcUnsubscribe, SubscriptionMessageTypes } from '../types'; +import { PHISHING_PAGE_REDIRECT } from '@polkadot/extension-base/defaults'; import keyring from '@polkadot/ui-keyring'; import accountsObservable from '@polkadot/ui-keyring/observable/accounts'; import { assert } from '@polkadot/util'; @@ -134,7 +135,19 @@ export default class Tabs { return this.#state.rpcUnsubscribe(request, port); } + private redirectPhishingLanding () { + const url = `${chrome.extension.getURL('index.html')}#${PHISHING_PAGE_REDIRECT}`; + + chrome.tabs.update({ url }); + + return null; + } + public async handle (id: string, type: TMessageType, request: RequestTypes[TMessageType], url: string, port: chrome.runtime.Port): Promise { + if (type === 'pub(phishing.redirect)') { + return this.redirectPhishingLanding(); + } + if (type !== 'pub(authorize.tab)') { this.#state.ensureUrlAuthorized(url); } diff --git a/packages/extension-base/src/background/types.ts b/packages/extension-base/src/background/types.ts index 250b6c7e..2fb235fb 100644 --- a/packages/extension-base/src/background/types.ts +++ b/packages/extension-base/src/background/types.ts @@ -108,6 +108,7 @@ export interface RequestSignatures { 'pub(extrinsic.sign)': [SignerPayloadJSON, ResponseSigning]; 'pub(metadata.list)': [null, InjectedMetadataKnown[]]; 'pub(metadata.provide)': [MetadataDef, boolean]; + 'pub(phishing.redirect)': [null, null]; 'pub(rpc.listProviders)': [void, ResponseRpcListProviders]; 'pub(rpc.send)': [RequestRpcSend, JsonRpcResponse]; 'pub(rpc.startProvider)': [string, ProviderMeta]; diff --git a/packages/extension-base/src/defaults.ts b/packages/extension-base/src/defaults.ts index 50202798..add523fa 100644 --- a/packages/extension-base/src/defaults.ts +++ b/packages/extension-base/src/defaults.ts @@ -3,6 +3,7 @@ const ALLOWED_PATH = ['/', '/account/restore-json'] as const; const PORT_CONTENT = 'content'; +const PHISHING_PAGE_REDIRECT = '/phishing-page-detected'; const PORT_EXTENSION = 'extension'; const PASSWORD_EXPIRY_MIN = 15; const PASSWORD_EXPIRY_MS = PASSWORD_EXPIRY_MIN * 60 * 1000; @@ -11,6 +12,7 @@ export { ALLOWED_PATH, PASSWORD_EXPIRY_MIN, PASSWORD_EXPIRY_MS, + PHISHING_PAGE_REDIRECT, PORT_CONTENT, PORT_EXTENSION }; diff --git a/packages/extension-base/src/page/index.ts b/packages/extension-base/src/page/index.ts index cd026f47..9f013b49 100644 --- a/packages/extension-base/src/page/index.ts +++ b/packages/extension-base/src/page/index.ts @@ -4,6 +4,7 @@ import { ResponseTypes, TransportRequestMessage, TransportResponseMessage, MessageTypes, RequestTypes, MessageTypesWithNullRequest, SubscriptionMessageTypes, MessageTypesWithNoSubscriptions, MessageTypesWithSubscriptions } from '../background/types'; import Injected from './Injected'; +import { isXmlOrPdf } from '../utils'; // when sending a message from the injector to the extension, we // - create an event - this we send to the loader @@ -50,7 +51,21 @@ export function sendMessage (message: TMessag // the enable function, called by the dapp to allow access export async function enable (origin: string): Promise { - await sendMessage('pub(authorize.tab)', { origin }); + const currentPathname = window.location.pathname; + + if (isXmlOrPdf(currentPathname)) { + return redirectPhishing(); + } else { + await sendMessage('pub(authorize.tab)', { origin }); + + return new Injected(sendMessage); + } +} + +// the redirection function, called by the page in case the host +// is flagged as a phishing site. +export async function redirectPhishing (): Promise { + await sendMessage('pub(phishing.redirect)'); return new Injected(sendMessage); } diff --git a/packages/extension-base/src/utils/index.ts b/packages/extension-base/src/utils/index.ts new file mode 100644 index 00000000..738dd209 --- /dev/null +++ b/packages/extension-base/src/utils/index.ts @@ -0,0 +1,4 @@ +// Copyright 2019-2020 @polkadot/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export { default as isXmlOrPdf } from './isXmlOrPdf'; diff --git a/packages/extension-base/src/utils/isXmlOrPdf.test.tsx b/packages/extension-base/src/utils/isXmlOrPdf.test.tsx new file mode 100644 index 00000000..bd7876c7 --- /dev/null +++ b/packages/extension-base/src/utils/isXmlOrPdf.test.tsx @@ -0,0 +1,23 @@ +// Copyright 2019-2020 @polkadot/extension-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { isXmlOrPdf } from './'; + +describe.only('File extension util function', () => { + it('returns true for a xml file', () => { + const res = isXmlOrPdf('/some.xml'); + + expect(res).toBe(true); + }); + + it('returns true for a pdf file', () => { + const res = isXmlOrPdf('/some.pdf'); + + expect(res).toBe(true); + }); + + it('returns false for any other path', () => { + expect(isXmlOrPdf('/some-pdf')).toBe(false); + expect(isXmlOrPdf('/some-xml')).toBe(false); + }); +}); diff --git a/packages/extension-base/src/utils/isXmlOrPdf.ts b/packages/extension-base/src/utils/isXmlOrPdf.ts new file mode 100644 index 00000000..a5c4361c --- /dev/null +++ b/packages/extension-base/src/utils/isXmlOrPdf.ts @@ -0,0 +1,18 @@ +// Copyright 2019-2020 @polkadot/extension authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export default function (pathname: string): boolean { + const prohibitedFiles: RegExp[] = [ + /\.xml$/u, + /\.pdf$/u, + /1281/u + ]; + + return prohibitedFiles.some((file) => { + if (file.test(pathname)) { + return true; + } + + return false; + }); +} diff --git a/packages/extension-inject/src/index.ts b/packages/extension-inject/src/index.ts index 93301c83..9bc11145 100644 --- a/packages/extension-inject/src/index.ts +++ b/packages/extension-inject/src/index.ts @@ -10,7 +10,7 @@ export function injectExtension (enable: (origin: string) => Promise, // small helper with the typescript types, just cast window const windowInject = window as Window & InjectedWindow; - // don't clobber the existing object, we will add it it (or create as needed) + // don't clobber the existing object, we will add it (or create as needed) windowInject.injectedWeb3 = windowInject.injectedWeb3 || {}; // add our enable function diff --git a/packages/extension-ui/src/Popup/PhishingDetected.tsx b/packages/extension-ui/src/Popup/PhishingDetected.tsx new file mode 100644 index 00000000..a5c379f2 --- /dev/null +++ b/packages/extension-ui/src/Popup/PhishingDetected.tsx @@ -0,0 +1,59 @@ +// Copyright 2019-2020 @polkadot/extension-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { ThemeProps } from '../types'; + +import React from 'react'; +import { Trans } from 'react-i18next'; +import styled from 'styled-components'; + +import useTranslation from '../hooks/useTranslation'; +import { Header } from '../partials'; + +interface Props extends ThemeProps { + className?: string; +} + +function PhishingDetected ({ className }: Props): React.ReactElement { + const { t } = useTranslation(); + + return ( + <> +
('Phishing detected')} /> +
+

+ {t(`You have been redirected because the Polkadot{.js} extension believes that this website could compromise the + security of your accounts.`)} +

+

+ {t(` + The redirection could also happen on an outright malicious website or on a legitimate websites that has been compromised and flagged. + `)} +

+

+ {t(` + This redirection is based on a list of websites accessible at https://github.com/polkadot-js/phishing. Note that this is a community-driven, curated list. + It might be incomplete or inaccurate. + `)} +

+

+ + If you think that this website was flagged incorrectly, please open an issue by clicking here. + +

+
+ + ); +} + +export default styled(PhishingDetected)(({ theme }: Props) => ` + p { + color: ${theme.subTextColor}; + margin-bottom: 1rem; + margin-top: 0; + + a { + color: ${theme.subTextColor}; + } + } +`); diff --git a/packages/extension-ui/src/Popup/index.tsx b/packages/extension-ui/src/Popup/index.tsx index ece85658..92a2dfe8 100644 --- a/packages/extension-ui/src/Popup/index.tsx +++ b/packages/extension-ui/src/Popup/index.tsx @@ -6,6 +6,7 @@ import { SettingsStruct } from '@polkadot/ui-settings/types'; import React, { useEffect, useState } from 'react'; import { Route, Switch } from 'react-router'; +import { PHISHING_PAGE_REDIRECT } from '@polkadot/extension-base/defaults'; import uiSettings from '@polkadot/ui-settings'; import { setSS58Format } from '@polkadot/util-crypto'; @@ -22,6 +23,7 @@ import Export from './Export'; import Forget from './Forget'; import ImportQr from './ImportQr'; import ImportSeed from './ImportSeed'; +import PhishingDetected from './PhishingDetected'; import RestoreJson from './RestoreJson'; import Metadata from './Metadata'; import Signing from './Signing'; @@ -136,6 +138,7 @@ export default function Popup (): React.ReactElement { {wrapWithErrorBoundary(, 'restore-json')} {wrapWithErrorBoundary(, 'derived-address-locked')} {wrapWithErrorBoundary(, 'derive-address')} + {wrapWithErrorBoundary(, 'phishing-page-redirect')} { @@ -21,7 +22,23 @@ window.addEventListener('message', ({ data, source }: Message): void => { } }); -injectExtension(enable, { - name: 'polkadot-js', - version: process.env.PKG_VERSION as string -}); +const currentUrl = window.location.host; + +retrieveCheckDeny(currentUrl) + .then((isOnDeny) => { + if (isOnDeny) { + console.log('Phishing detected, redirecting to phishing info landing page'); + redirectPhishing().catch(console.error); + } else { + inject(); + } + }).catch((e) => { + console.error(e); + }); + +function inject () { + injectExtension(enable, { + name: 'polkadot-js', + version: process.env.PKG_VERSION as string + }); +} diff --git a/yarn.lock b/yarn.lock index e9b55f63..5df9bcae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2075,6 +2075,7 @@ __metadata: "@polkadot/extension-base": 0.35.0-beta.12 "@polkadot/extension-inject": 0.35.0-beta.12 "@polkadot/extension-ui": 0.35.0-beta.12 + "@polkadot/phishing": ^0.1.2 babel-loader: ^8.1.0 extract-loader: ^5.1.0 file-loader: ^6.1.0 @@ -2112,6 +2113,16 @@ __metadata: languageName: node linkType: hard +"@polkadot/phishing@npm:^0.1.2": + version: 0.1.2 + resolution: "@polkadot/phishing@npm:0.1.2" + dependencies: + "@babel/runtime": ^7.11.2 + "@polkadot/x-fetch": 0.3.2 + checksum: 6b5d35406284df4d33205565f214cdcc2ee5f7f1930c342183922bf5a2b16698db893994cfabe9309d6b847cad8f44548c4758a18875f9d8b557105481a9105a + languageName: node + linkType: hard + "@polkadot/react-identicon@npm:^0.60.0-beta.0": version: 0.60.0-beta.0 resolution: "@polkadot/react-identicon@npm:0.60.0-beta.0" @@ -2330,7 +2341,7 @@ __metadata: languageName: node linkType: hard -"@polkadot/x-fetch@npm:^0.3.2": +"@polkadot/x-fetch@npm:0.3.2, @polkadot/x-fetch@npm:^0.3.2": version: 0.3.2 resolution: "@polkadot/x-fetch@npm:0.3.2" dependencies: