From 26619e28cc7c2fbea89380cd1d30ab93751700be Mon Sep 17 00:00:00 2001
From: Jack Clancy
Date: Wed, 10 Jan 2018 00:17:52 -0500
Subject: [PATCH] Enforce HTTPS / Prevent Reverse Tabnabbing (#773)
* working version of test custom rule config
* setting no imports to false so tests will pass
* adding anchor blank noopener rule, rule currently off to allow tests to pass
* removing copied code from tslint-microsoft-contrib
* adding tslint-microsoft-contrib to dev deps
* extending tslint for external http rule
* locking tslint-microsoft-contrib version and turning on target blank noopener rule
* final fixes for pull #663
* add noopener noreferrer as needed
* fixing false positives for a tags without href
* really fix linting errors
* fix imports
* remove accidently(?) added LedgerNano duplicate file
---
.../components/BalanceSidebar/AccountInfo.tsx | 12 ++-
common/components/BalanceSidebar/Promos.tsx | 1 +
common/components/ErrorScreen/index.tsx | 2 +-
.../TransactionSucceeded.tsx | 7 +-
.../Header/components/GasPriceDropdown.tsx | 6 +-
.../Header/components/NavigationLink.tsx | 8 +-
.../components/DeterministicWalletsModal.tsx | 10 +-
.../WalletDecrypt/components/LedgerNano.tsx | 10 +-
.../WalletDecrypt/components/Mnemonic.tsx | 2 +-
.../WalletDecrypt/components/Trezor.tsx | 6 +-
common/components/ui/Help.tsx | 2 +-
common/components/ui/NewTabLink.tsx | 2 +-
common/containers/Tabs/Help/index.tsx | 3 +-
.../Tabs/Swap/components/BitcoinQR.tsx | 2 +-
.../Tabs/Swap/components/CurrentRates.tsx | 7 +-
.../Swap/components/SwapInfoHeaderTitle.tsx | 7 +-
.../Tabs/Swap/components/SwapProgress.tsx | 4 +-
common/index.html | 8 +-
.../noExternalHttpLinkRule.js | 95 ++++++++++++++++++
.../noExternalHttpLinkRule.ts | 96 +++++++++++++++++++
package.json | 1 +
tslint.json | 6 +-
22 files changed, 266 insertions(+), 31 deletions(-)
create mode 100644 custom_linting_rules/noExternalHttpLinkRule.js
create mode 100644 custom_linting_rules/noExternalHttpLinkRule.ts
diff --git a/common/components/BalanceSidebar/AccountInfo.tsx b/common/components/BalanceSidebar/AccountInfo.tsx
index 074a2188..e363b4e3 100644
--- a/common/components/BalanceSidebar/AccountInfo.tsx
+++ b/common/components/BalanceSidebar/AccountInfo.tsx
@@ -93,14 +93,22 @@ export default class AccountInfo extends React.Component {
{!!blockExplorer && (
-
-
+
{`${network.name} (${blockExplorer.name})`}
)}
{!!tokenExplorer && (
-
-
+
{`Tokens (${tokenExplorer.name})`}
diff --git a/common/components/BalanceSidebar/Promos.tsx b/common/components/BalanceSidebar/Promos.tsx
index 06693d12..6c4ac07c 100644
--- a/common/components/BalanceSidebar/Promos.tsx
+++ b/common/components/BalanceSidebar/Promos.tsx
@@ -58,6 +58,7 @@ export default class Promos extends React.Component<{}, State> {
className="Promos-promo"
key={promo.href}
target="_blank"
+ rel="noopener noreferrer"
href={promo.href}
style={{ backgroundColor: promo.color }}
>
diff --git a/common/components/ErrorScreen/index.tsx b/common/components/ErrorScreen/index.tsx
index bb263c4f..643283b3 100644
--- a/common/components/ErrorScreen/index.tsx
+++ b/common/components/ErrorScreen/index.tsx
@@ -19,7 +19,7 @@ const ErrorScreen: React.SFC = ({ error }) => {
Please contact{' '}
support@myetherwallet.com
diff --git a/common/components/ExtendedNotifications/TransactionSucceeded.tsx b/common/components/ExtendedNotifications/TransactionSucceeded.tsx
index 8385db35..074da8d0 100644
--- a/common/components/ExtendedNotifications/TransactionSucceeded.tsx
+++ b/common/components/ExtendedNotifications/TransactionSucceeded.tsx
@@ -14,7 +14,12 @@ const TransactionSucceeded = ({ txHash, blockExplorer }: TransactionSucceededPro
return (
diff --git a/common/components/Header/components/GasPriceDropdown.tsx b/common/components/Header/components/GasPriceDropdown.tsx
index 7110a9af..a2610815 100644
--- a/common/components/Header/components/GasPriceDropdown.tsx
+++ b/common/components/Header/components/GasPriceDropdown.tsx
@@ -61,7 +61,11 @@ export default class GasPriceDropdown extends Component {
21 GWEI
.
-
+
Read more
diff --git a/common/components/Header/components/NavigationLink.tsx b/common/components/Header/components/NavigationLink.tsx
index 8d6f0a48..42ca117e 100644
--- a/common/components/Header/components/NavigationLink.tsx
+++ b/common/components/Header/components/NavigationLink.tsx
@@ -35,7 +35,13 @@ class NavigationLink extends React.Component {
const linkEl =
link.external || !link.to ? (
-
+
{translate(link.name)}
) : (
diff --git a/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx b/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx
index 15e502ae..769d088a 100644
--- a/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx
+++ b/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx
@@ -292,7 +292,11 @@ class DeterministicWalletsModalClass extends React.Component {
)}
-
+
|
@@ -310,7 +314,9 @@ function mapStateToProps(state: AppState) {
};
}
-export const DeterministicWalletsModal = connect(mapStateToProps, {
+const DeterministicWalletsModal = connect(mapStateToProps, {
getDeterministicWallets,
setDesiredToken
})(DeterministicWalletsModalClass);
+
+export default DeterministicWalletsModal;
diff --git a/common/components/WalletDecrypt/components/LedgerNano.tsx b/common/components/WalletDecrypt/components/LedgerNano.tsx
index ef993076..7d97c49e 100644
--- a/common/components/WalletDecrypt/components/LedgerNano.tsx
+++ b/common/components/WalletDecrypt/components/LedgerNano.tsx
@@ -1,7 +1,7 @@
import './LedgerNano.scss';
import React, { Component } from 'react';
import translate, { translateRaw } from 'translations';
-import { DeterministicWalletsModal } from './DeterministicWalletsModal';
+import DeterministicWalletsModal from './DeterministicWalletsModal';
import { LedgerWallet } from 'libs/wallet';
import Ledger3 from 'vendor/ledger3';
import LedgerEth from 'vendor/ledger-eth';
@@ -81,7 +81,7 @@ export class LedgerNanoSDecrypt extends Component {
className="LedgerDecrypt-buy btn btn-sm btn-default"
href="https://www.ledgerwallet.com/r/fa4b?path=/products/"
target="_blank"
- rel="noopener"
+ rel="noopener noreferrer"
>
{translate('Don’t have a Ledger? Order one now!')}
@@ -92,9 +92,9 @@ export class LedgerNanoSDecrypt extends Component {
Guides:
How to use MyEtherWallet with your Nano S
@@ -103,7 +103,7 @@ export class LedgerNanoSDecrypt extends Component
{
How to secure your tokens with your Nano S
diff --git a/common/components/WalletDecrypt/components/Mnemonic.tsx b/common/components/WalletDecrypt/components/Mnemonic.tsx
index 0a880807..b9fd2ca8 100644
--- a/common/components/WalletDecrypt/components/Mnemonic.tsx
+++ b/common/components/WalletDecrypt/components/Mnemonic.tsx
@@ -2,7 +2,7 @@ import { mnemonicToSeed, validateMnemonic } from 'bip39';
import DPATHS from 'config/dpaths';
import React, { Component } from 'react';
import translate, { translateRaw } from 'translations';
-import { DeterministicWalletsModal } from './DeterministicWalletsModal';
+import DeterministicWalletsModal from './DeterministicWalletsModal';
import { formatMnemonic } from 'utils/formatters';
const DEFAULT_PATH = DPATHS.MNEMONIC[0].value;
diff --git a/common/components/WalletDecrypt/components/Trezor.tsx b/common/components/WalletDecrypt/components/Trezor.tsx
index aea42c2c..26971a5b 100644
--- a/common/components/WalletDecrypt/components/Trezor.tsx
+++ b/common/components/WalletDecrypt/components/Trezor.tsx
@@ -3,7 +3,7 @@ import { TrezorWallet } from 'libs/wallet';
import React, { Component } from 'react';
import translate, { translateRaw } from 'translations';
import TrezorConnect from 'vendor/trezor-connect';
-import { DeterministicWalletsModal } from './DeterministicWalletsModal';
+import DeterministicWalletsModal from './DeterministicWalletsModal';
import './Trezor.scss';
import { Spinner } from 'components/ui';
const DEFAULT_PATH = DPATHS.TREZOR[0].value;
@@ -53,7 +53,7 @@ export class TrezorDecrypt extends Component {
className="TrezorDecrypt-buy btn btn-sm btn-default"
href="https://trezor.io/?a=myetherwallet.com"
target="_blank"
- rel="noopener"
+ rel="noopener noreferrer"
>
{translate('Don’t have a TREZOR? Order one now!')}
@@ -65,7 +65,7 @@ export class TrezorDecrypt extends Component {
How to use TREZOR with MyEtherWallet
diff --git a/common/components/ui/Help.tsx b/common/components/ui/Help.tsx
index 3cea97b1..de018200 100644
--- a/common/components/ui/Help.tsx
+++ b/common/components/ui/Help.tsx
@@ -11,7 +11,7 @@ interface Props {
const Help = ({ size = 'x1', link }: Props) => {
return (
-
+
);
diff --git a/common/components/ui/NewTabLink.tsx b/common/components/ui/NewTabLink.tsx
index b93da2d0..cbd11930 100644
--- a/common/components/ui/NewTabLink.tsx
+++ b/common/components/ui/NewTabLink.tsx
@@ -36,7 +36,7 @@ interface NewTabLinkProps extends AAttributes {
}
const NewTabLink = ({ content, children, ...rest }: NewTabLinkProps) => (
-
+
{content || children}
);
diff --git a/common/containers/Tabs/Help/index.tsx b/common/containers/Tabs/Help/index.tsx
index 4a74cd4a..6bba17dc 100644
--- a/common/containers/Tabs/Help/index.tsx
+++ b/common/containers/Tabs/Help/index.tsx
@@ -17,6 +17,7 @@ const Help = () => (
{translate('HELP_Warning')}
@@ -25,7 +26,7 @@ const Help = () => (
This page is deprecated. Please check out our more up-to-date and searchable{' '}
-
+
Knowledge Base.{' '}
diff --git a/common/containers/Tabs/Swap/components/BitcoinQR.tsx b/common/containers/Tabs/Swap/components/BitcoinQR.tsx
index 39f29758..27cd3a51 100644
--- a/common/containers/Tabs/Swap/components/BitcoinQR.tsx
+++ b/common/containers/Tabs/Swap/components/BitcoinQR.tsx
@@ -21,7 +21,7 @@ export default class BitcoinQR extends Component {
Orders that take too long will have to be processed manually & and may delay the
amount of time it takes to receive your coins.
-
+
Please use the recommended TX fees seen here.
diff --git a/common/containers/Tabs/Swap/components/CurrentRates.tsx b/common/containers/Tabs/Swap/components/CurrentRates.tsx
index 6c7686c3..58ca0540 100644
--- a/common/containers/Tabs/Swap/components/CurrentRates.tsx
+++ b/common/containers/Tabs/Swap/components/CurrentRates.tsx
@@ -88,7 +88,12 @@ export default class CurrentRates extends Component {
{children}
-
+
diff --git a/common/containers/Tabs/Swap/components/SwapInfoHeaderTitle.tsx b/common/containers/Tabs/Swap/components/SwapInfoHeaderTitle.tsx
index d0b37a97..27e86ed8 100644
--- a/common/containers/Tabs/Swap/components/SwapInfoHeaderTitle.tsx
+++ b/common/containers/Tabs/Swap/components/SwapInfoHeaderTitle.tsx
@@ -27,7 +27,12 @@ export default class SwapInfoHeaderTitle extends Component{translate('SWAP_information')}
diff --git a/common/containers/Tabs/Swap/components/SwapProgress.tsx b/common/containers/Tabs/Swap/components/SwapProgress.tsx
index ce390c59..a84789ab 100644
--- a/common/containers/Tabs/Swap/components/SwapProgress.tsx
+++ b/common/containers/Tabs/Swap/components/SwapProgress.tsx
@@ -49,7 +49,7 @@ export default class SwapProgress extends Component {
if (destinationId !== 'BTC') {
link = bityConfig.ETHTxExplorer(outputTx);
linkElement = (
-
+
{notificationMessage}
);
@@ -57,7 +57,7 @@ export default class SwapProgress extends Component {
} else {
link = bityConfig.BTCTxExplorer(outputTx);
linkElement = (
-
+
{notificationMessage}
);
diff --git a/common/index.html b/common/index.html
index d77f5fbf..69e84eb1 100644
--- a/common/index.html
+++ b/common/index.html
@@ -22,7 +22,7 @@
If you are not sure why you are seeing this message, or are unsure of how to enable Javascript, please visit
- enable-javascript.com
+ enable-javascript.com
to learn more.
@@ -41,18 +41,18 @@
to a laptop or computer to continue using MyEtherWallet.
-
+
Firefox
-
Chrome
-
+
Opera
diff --git a/custom_linting_rules/noExternalHttpLinkRule.js b/custom_linting_rules/noExternalHttpLinkRule.js
new file mode 100644
index 00000000..45741cfd
--- /dev/null
+++ b/custom_linting_rules/noExternalHttpLinkRule.js
@@ -0,0 +1,95 @@
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+ var extendStatics = Object.setPrototypeOf ||
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+ function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+ return function (d, b) {
+ extendStatics(d, b);
+ function __() { this.constructor = d; }
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+ };
+})();
+exports.__esModule = true;
+var ts = require("typescript");
+var Lint = require("tslint");
+var ErrorTolerantWalker_1 = require("../node_modules/tslint-microsoft-contrib/utils/ErrorTolerantWalker");
+var JsxAttribute_1 = require("../node_modules/tslint-microsoft-contrib/utils/JsxAttribute");
+var FAILURE_STRING = 'Anchor tags with an external link must use https';
+/**
+ * Implementation of the no-external-http-link rule.
+ */
+var Rule = /** @class */ (function (_super) {
+ __extends(Rule, _super);
+ function Rule() {
+ return _super !== null && _super.apply(this, arguments) || this;
+ }
+ Rule.prototype.apply = function (sourceFile) {
+ if (sourceFile.languageVariant === ts.LanguageVariant.JSX) {
+ return this.applyWithWalker(new NoExternalHttpLinkRuleWalker(sourceFile, this.getOptions()));
+ }
+ else {
+ return [];
+ }
+ };
+ Rule.metadata = {
+ ruleName: 'tno-external-http-link',
+ type: 'functionality',
+ description: 'Anchor tags with an external link must use https',
+ options: null,
+ optionsDescription: '',
+ typescriptOnly: true,
+ issueClass: 'SDL',
+ issueType: 'Error',
+ severity: 'Critical',
+ level: 'Mandatory',
+ group: 'Security',
+ commonWeaknessEnumeration: '242,676'
+ };
+ return Rule;
+}(Lint.Rules.AbstractRule));
+exports.Rule = Rule;
+var NoExternalHttpLinkRuleWalker = /** @class */ (function (_super) {
+ __extends(NoExternalHttpLinkRuleWalker, _super);
+ function NoExternalHttpLinkRuleWalker() {
+ return _super !== null && _super.apply(this, arguments) || this;
+ }
+ NoExternalHttpLinkRuleWalker.prototype.visitJsxElement = function (node) {
+ var openingElement = node.openingElement;
+ this.validateOpeningElement(openingElement);
+ _super.prototype.visitJsxElement.call(this, node);
+ };
+ NoExternalHttpLinkRuleWalker.prototype.visitJsxSelfClosingElement = function (node) {
+ this.validateOpeningElement(node);
+ _super.prototype.visitJsxSelfClosingElement.call(this, node);
+ };
+ NoExternalHttpLinkRuleWalker.prototype.validateOpeningElement = function (openingElement) {
+ if (openingElement.tagName.getText() === 'a') {
+ var allAttributes = JsxAttribute_1.getJsxAttributesFromJsxElement(openingElement);
+ var href = allAttributes.href;
+ if (href !== null && !isSafeHrefAttributeValue(href) && JsxAttribute_1.getStringLiteral(href) !== 'undefined') {
+ this.addFailureAt(openingElement.getStart(), openingElement.getWidth(), FAILURE_STRING);
+ }
+ }
+ };
+ return NoExternalHttpLinkRuleWalker;
+}(ErrorTolerantWalker_1.ErrorTolerantWalker));
+function isSafeHrefAttributeValue(attribute) {
+ if (JsxAttribute_1.isEmpty(attribute)) {
+ return false;
+ }
+ if (attribute.initializer.kind === ts.SyntaxKind.JsxExpression) {
+ var expression = attribute.initializer;
+ if (expression.expression !== null &&
+ expression.expression.kind !== ts.SyntaxKind.StringLiteral) {
+ return true; // attribute value is not a string literal, so do not validate
+ }
+ }
+ var stringValue = JsxAttribute_1.getStringLiteral(attribute);
+ if (stringValue === '#') {
+ return true;
+ }
+ else if (stringValue === null || stringValue.length === 0) {
+ return false;
+ }
+ return stringValue.indexOf('https://') >= 0;
+}
diff --git a/custom_linting_rules/noExternalHttpLinkRule.ts b/custom_linting_rules/noExternalHttpLinkRule.ts
new file mode 100644
index 00000000..8f88e2c2
--- /dev/null
+++ b/custom_linting_rules/noExternalHttpLinkRule.ts
@@ -0,0 +1,96 @@
+import * as ts from 'typescript';
+import * as Lint from 'tslint';
+
+import { ErrorTolerantWalker } from '../node_modules/tslint-microsoft-contrib/utils/ErrorTolerantWalker';
+import { ExtendedMetadata } from '../node_modules/tslint-microsoft-contrib/utils/ExtendedMetadata';
+import { Utils } from '../node_modules/tslint-microsoft-contrib/utils/Utils';
+
+import {
+ getJsxAttributesFromJsxElement,
+ getStringLiteral,
+ isEmpty
+} from '../node_modules/tslint-microsoft-contrib/utils/JsxAttribute';
+
+const FAILURE_STRING = 'Anchor tags with an external link must use https';
+
+/**
+ * Implementation of the no-external-http-link rule.
+ */
+export class Rule extends Lint.Rules.AbstractRule {
+ public static metadata: ExtendedMetadata = {
+ ruleName: 'tno-external-http-link',
+ type: 'functionality',
+ description: 'Anchor tags with an external link must use https',
+ options: null,
+ optionsDescription: '',
+ typescriptOnly: true,
+ issueClass: 'SDL',
+ issueType: 'Error',
+ severity: 'Critical',
+ level: 'Mandatory',
+ group: 'Security',
+ commonWeaknessEnumeration: '242,676'
+ };
+
+ public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
+ if (sourceFile.languageVariant === ts.LanguageVariant.JSX) {
+ return this.applyWithWalker(new NoExternalHttpLinkRuleWalker(sourceFile, this.getOptions()));
+ } else {
+ return [];
+ }
+ }
+}
+
+class NoExternalHttpLinkRuleWalker extends ErrorTolerantWalker {
+ protected visitJsxElement(node: ts.JsxElement): void {
+ const openingElement: ts.JsxOpeningElement = node.openingElement;
+ this.validateOpeningElement(openingElement);
+ super.visitJsxElement(node);
+ }
+
+ protected visitJsxSelfClosingElement(node: ts.JsxSelfClosingElement): void {
+ this.validateOpeningElement(node);
+ super.visitJsxSelfClosingElement(node);
+ }
+
+ private validateOpeningElement(openingElement: ts.JsxOpeningLikeElement): void {
+ if (openingElement.tagName.getText() === 'a') {
+ const allAttributes: { [propName: string]: ts.JsxAttribute } = getJsxAttributesFromJsxElement(
+ openingElement
+ );
+ const href: ts.JsxAttribute = allAttributes.href;
+ if (
+ href !== null &&
+ !isSafeHrefAttributeValue(href) &&
+ getStringLiteral(href) !== 'undefined'
+ ) {
+ this.addFailureAt(openingElement.getStart(), openingElement.getWidth(), FAILURE_STRING);
+ }
+ }
+ }
+}
+
+function isSafeHrefAttributeValue(attribute: ts.JsxAttribute): boolean {
+ if (isEmpty(attribute)) {
+ return false;
+ }
+
+ if (attribute.initializer.kind === ts.SyntaxKind.JsxExpression) {
+ const expression: ts.JsxExpression = attribute.initializer;
+ if (
+ expression.expression !== null &&
+ expression.expression.kind !== ts.SyntaxKind.StringLiteral
+ ) {
+ return true; // attribute value is not a string literal, so do not validate
+ }
+ }
+
+ const stringValue = getStringLiteral(attribute);
+ if (stringValue === '#') {
+ return true;
+ } else if (stringValue === null || stringValue.length === 0) {
+ return false;
+ }
+
+ return stringValue.indexOf('https://') >= 0;
+}
diff --git a/package.json b/package.json
index 918b0429..6b19cf05 100644
--- a/package.json
+++ b/package.json
@@ -109,6 +109,7 @@
"ts-loader": "3.2.0",
"tslint": "5.8.0",
"tslint-config-prettier": "1.6.0",
+ "tslint-microsoft-contrib": "5.0.1",
"tslint-react": "3.3.3",
"types-rlp": "0.0.1",
"typescript": "2.6.2",
diff --git a/tslint.json b/tslint.json
index 1f943613..122d5c9b 100644
--- a/tslint.json
+++ b/tslint.json
@@ -23,7 +23,9 @@
"no-var-requires": false,
"jsx-wrap-multiline": false,
"comment-format": false,
- "ordered-imports": false
+ "ordered-imports": false,
+ "react-anchor-blank-noopener": true,
+ "no-external-http-link": true
},
- "rulesDirectory": []
+ "rulesDirectory": ["node_modules/tslint-microsoft-contrib", "custom_linting_rules"]
}