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
This commit is contained in:
Jack Clancy 2018-01-10 00:17:52 -05:00 committed by Daniel Ternyak
parent 6e2b74c79a
commit 26619e28cc
22 changed files with 266 additions and 31 deletions

View File

@ -93,14 +93,22 @@ export default class AccountInfo extends React.Component<Props, State> {
<ul className="AccountInfo-list">
{!!blockExplorer && (
<li className="AccountInfo-list-item">
<a href={blockExplorer.address(address)} target="_blank">
<a
href={blockExplorer.address(address)}
target="_blank"
rel="noopener noreferrer"
>
{`${network.name} (${blockExplorer.name})`}
</a>
</li>
)}
{!!tokenExplorer && (
<li className="AccountInfo-list-item">
<a href={tokenExplorer.address(address)} target="_blank">
<a
href={tokenExplorer.address(address)}
target="_blank"
rel="noopener noreferrer"
>
{`Tokens (${tokenExplorer.name})`}
</a>
</li>

View File

@ -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 }}
>

View File

@ -19,7 +19,7 @@ const ErrorScreen: React.SFC<Props> = ({ error }) => {
Please contact{' '}
<a
target="_blank"
rel="noopener"
rel="noopener noreferrer"
href={`mailto:support@myetherwallet.com?Subject=${SUBJECT}&body=${DESCRIPTION}`}
>
support@myetherwallet.com

View File

@ -14,7 +14,12 @@ const TransactionSucceeded = ({ txHash, blockExplorer }: TransactionSucceededPro
return (
<div>
<p>{translateRaw('SUCCESS_3') + txHash}</p>
<a className="btn btn-xs btn-info string" href={txHashLink} target="_blank" rel="noopener">
<a
className="btn btn-xs btn-info string"
href={txHashLink}
target="_blank"
rel="noopener noreferrer"
>
Verify Transaction
</a>
</div>

View File

@ -61,7 +61,11 @@ export default class GasPriceDropdown extends Component<Props> {
<code>21 GWEI</code>.
</p>
<p>
<a href={`${knowledgeBaseURL}/gas/what-is-gas-ethereum`} target="_blank">
<a
href={`${knowledgeBaseURL}/gas/what-is-gas-ethereum`}
target="_blank"
rel="noopener noreferrer"
>
Read more
</a>
</p>

View File

@ -35,7 +35,13 @@ class NavigationLink extends React.Component<Props, {}> {
const linkEl =
link.external || !link.to ? (
<a className={linkClasses} href={link.to} aria-label={linkLabel} target="_blank">
<a
className={linkClasses}
href={link.to}
aria-label={linkLabel}
target="_blank"
rel="noopener noreferrer"
>
{translate(link.name)}
</a>
) : (

View File

@ -292,7 +292,11 @@ class DeterministicWalletsModalClass extends React.Component<Props, State> {
)}
</td>
<td>
<a target="_blank" href={`https://ethplorer.io/address/${wallet.address}`}>
<a
target="_blank"
href={`https://ethplorer.io/address/${wallet.address}`}
rel="noopener noreferrer"
>
<i className="DWModal-addresses-table-more" />
</a>
</td>
@ -310,7 +314,9 @@ function mapStateToProps(state: AppState) {
};
}
export const DeterministicWalletsModal = connect(mapStateToProps, {
const DeterministicWalletsModal = connect(mapStateToProps, {
getDeterministicWallets,
setDesiredToken
})(DeterministicWalletsModalClass);
export default DeterministicWalletsModal;

View File

@ -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<Props, State> {
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('Dont have a Ledger? Order one now!')}
</a>
@ -92,9 +92,9 @@ export class LedgerNanoSDecrypt extends Component<Props, State> {
Guides:
<div>
<a
href="http://support.ledgerwallet.com/knowledge_base/topics/how-to-use-myetherwallet-with-ledger"
href="https://support.ledgerwallet.com/knowledge_base/topics/how-to-use-myetherwallet-with-ledger"
target="_blank"
rel="noopener"
rel="noopener noreferrer"
>
How to use MyEtherWallet with your Nano S
</a>
@ -103,7 +103,7 @@ export class LedgerNanoSDecrypt extends Component<Props, State> {
<a
href="https://ledger.groovehq.com/knowledge_base/topics/how-to-secure-your-eth-tokens-augur-rep-dot-dot-dot-with-your-nano-s"
target="_blank"
rel="noopener"
rel="noopener noreferrer"
>
How to secure your tokens with your Nano S
</a>

View File

@ -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;

View File

@ -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<Props, State> {
className="TrezorDecrypt-buy btn btn-sm btn-default"
href="https://trezor.io/?a=myetherwallet.com"
target="_blank"
rel="noopener"
rel="noopener noreferrer"
>
{translate('Dont have a TREZOR? Order one now!')}
</a>
@ -65,7 +65,7 @@ export class TrezorDecrypt extends Component<Props, State> {
<a
href="https://blog.trezor.io/trezor-integration-with-myetherwallet-3e217a652e08"
target="_blank"
rel="noopener"
rel="noopener noreferrer"
>
How to use TREZOR with MyEtherWallet
</a>

View File

@ -11,7 +11,7 @@ interface Props {
const Help = ({ size = 'x1', link }: Props) => {
return (
<a href={link} className={`Help Help-${size}`} target={'_blank'}>
<a href={link} className={`Help Help-${size}`} target="_blank" rel="noopener noreferrer">
<img src={icon} />
</a>
);

View File

@ -36,7 +36,7 @@ interface NewTabLinkProps extends AAttributes {
}
const NewTabLink = ({ content, children, ...rest }: NewTabLinkProps) => (
<a target="_blank" rel="noopener" {...rest}>
<a target="_blank" rel="noopener noreferrer" {...rest}>
{content || children}
</a>
);

View File

@ -17,6 +17,7 @@ const Help = () => (
<a
href="https://www.reddit.com/r/ethereum/comments/47nkoi/psa_check_your_ethaddressorg_wallets_and_any/d0eo45o"
target="_blank"
rel="noopener noreferrer"
>
<span className="text-danger">{translate('HELP_Warning')}</span>
</a>
@ -25,7 +26,7 @@ const Help = () => (
<li>
<h3>
This page is deprecated. Please check out our more up-to-date and searchable{' '}
<a href={knowledgeBaseURL} target="_blank">
<a href={knowledgeBaseURL} target="_blank" rel="noopener noreferrer">
Knowledge Base.{' '}
</a>
</h3>

View File

@ -21,7 +21,7 @@ export default class BitcoinQR extends Component<Props, {}> {
Orders that take too long will have to be processed manually &amp; and may delay the
amount of time it takes to receive your coins.
<br />
<a href="https://shapeshift.io/#/btcfee" target="_blank" rel="noopener">
<a href="https://shapeshift.io/#/btcfee" target="_blank" rel="noopener noreferrer">
Please use the recommended TX fees seen here.
</a>
</p>

View File

@ -88,7 +88,12 @@ export default class CurrentRates extends Component<Props> {
<section className="SwapRates-panel row">
{children}
<a className="SwapRates-panel-logo" href={providerURL} target="_blank">
<a
className="SwapRates-panel-logo"
href={providerURL}
target="_blank"
rel="noopener noreferrer"
>
<img src={providerLogo} width={120} height={49} />
</a>
</section>

View File

@ -27,7 +27,12 @@ export default class SwapInfoHeaderTitle extends Component<SwapInfoHeaderTitlePr
<h3 className="SwapInfo-top-title">{translate('SWAP_information')}</h3>
</div>
<div className="col-xs-3">
<a className="SwapInfo-top-logo" href={bityReferralURL} target="_blank" rel="noopener">
<a
className="SwapInfo-top-logo"
href={bityReferralURL}
target="_blank"
rel="noopener noreferrer"
>
<img className="SwapInfo-top-logo-img" src={logoToRender} />
</a>
</div>

View File

@ -49,7 +49,7 @@ export default class SwapProgress extends Component<Props, State> {
if (destinationId !== 'BTC') {
link = bityConfig.ETHTxExplorer(outputTx);
linkElement = (
<a href={link} target="_blank" rel="noopener">
<a href={link} target="_blank" rel="noopener noreferrer">
{notificationMessage}
</a>
);
@ -57,7 +57,7 @@ export default class SwapProgress extends Component<Props, State> {
} else {
link = bityConfig.BTCTxExplorer(outputTx);
linkElement = (
<a href={link} target="_blank" rel="noopener">
<a href={link} target="_blank" rel="noopener noreferrer">
{notificationMessage}
</a>
);

View File

@ -22,7 +22,7 @@
</p>
<p>
If you are not sure why you are seeing this message, or are unsure of how to enable Javascript, please visit
<a href="https://www.enable-javascript.com/" rel="noopener" target="_blank">enable-javascript.com</a>
<a href="https://www.enable-javascript.com/" rel="noopener noreferrer" target="_blank">enable-javascript.com</a>
to learn more.
</p>
</div>
@ -41,18 +41,18 @@
to a laptop or computer to continue using MyEtherWallet.
</p>
<div class="BadBrowser-content-browsers is-desktop">
<a class="BadBrowser-content-browsers-browser firefox" href="https://www.mozilla.org/en-US/firefox/new/" rel="noopener" target="_blank">
<a class="BadBrowser-content-browsers-browser firefox" href="https://www.mozilla.org/en-US/firefox/new/" rel="noopener noreferrer" target="_blank">
<span class="BadBrowser-content-browsers-browser-name">
Firefox
</span>
</a>
<a class="BadBrowser-content-browsers-browser chrome" href="https://www.google.com/chrome/browser/desktop/index.html" rel="noopener"
<a class="BadBrowser-content-browsers-browser chrome" href="https://www.google.com/chrome/browser/desktop/index.html" rel="noopener noreferrer"
target="_blank">
<span class="BadBrowser-content-browsers-browser-name">
Chrome
</span>
</a>
<a class="BadBrowser-content-browsers-browser opera" href="http://www.opera.com/" rel="noopener" target="_blank">
<a class="BadBrowser-content-browsers-browser opera" href="http://www.opera.com/" rel="noopener noreferrer" target="_blank">
<span class="BadBrowser-content-browsers-browser-name">
Opera
</span>

View File

@ -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;
}

View File

@ -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 = <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;
}

View File

@ -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",

View File

@ -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"]
}