This repository has been archived by the owner on Jul 15, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 198
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Issue #210] new rule: react-anchor-blank-noopener
closes #210
- Loading branch information
Showing
5 changed files
with
288 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import * as ts from 'typescript'; | ||
import * as Lint from 'tslint/lib/lint'; | ||
|
||
import {ErrorTolerantWalker} from './utils/ErrorTolerantWalker'; | ||
import {ExtendedMetadata} from './utils/ExtendedMetadata'; | ||
import {SyntaxKind} from './utils/SyntaxKind'; | ||
import {Utils} from './utils/Utils'; | ||
|
||
import { getJsxAttributesFromJsxElement, | ||
getStringLiteral, | ||
isEmpty } from './utils/JsxAttribute'; | ||
|
||
const FAILURE_STRING: string = 'Anchor tags with target="_blank" should also include rel="noopener noreferrer"'; | ||
|
||
/** | ||
* Implementation of the react-anchor-blank-noopener rule. | ||
*/ | ||
export class Rule extends Lint.Rules.AbstractRule { | ||
|
||
public static metadata: ExtendedMetadata = { | ||
ruleName: 'react-anchor-blank-noopener', | ||
type: 'functionality', | ||
description: 'Anchor tags with target="_blank" should also include rel="noopener noreferrer"', | ||
options: null, | ||
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 ReactAnchorBlankNoopenerRuleWalker(sourceFile, this.getOptions())); | ||
} else { | ||
return []; | ||
} | ||
} | ||
} | ||
|
||
class ReactAnchorBlankNoopenerRuleWalker 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.JsxOpeningElement): void { | ||
if (openingElement.tagName.getText() === 'a') { | ||
|
||
const allAttributes: { [propName: string]: ts.JsxAttribute } = getJsxAttributesFromJsxElement(openingElement); | ||
/* tslint:disable:no-string-literal */ | ||
const target: ts.JsxAttribute = allAttributes['target']; | ||
const rel: ts.JsxAttribute = allAttributes['rel']; | ||
/* tslint:enable:no-string-literal */ | ||
if (getStringLiteral(target) === '_blank' && !isRelAttributeValue(rel)) { | ||
this.addFailure(this.createFailure(openingElement.getStart(), openingElement.getWidth(), FAILURE_STRING)); | ||
} | ||
} | ||
} | ||
} | ||
|
||
function isRelAttributeValue(attribute: ts.JsxAttribute): boolean { | ||
if (isEmpty(attribute)) { | ||
return false; | ||
} | ||
|
||
if (attribute.initializer.kind === SyntaxKind.current().JsxExpression) { | ||
const expression: ts.JsxExpression = <ts.JsxExpression>attribute.initializer; | ||
if (expression.expression != null && expression.expression.kind !== SyntaxKind.current().StringLiteral) { | ||
return true; // attribute value is not a string literal, so do not validate | ||
} | ||
} | ||
|
||
const stringValue = getStringLiteral(attribute); | ||
if (stringValue == null || stringValue.length === 0) { | ||
return false; | ||
} | ||
|
||
const relValues: string[] = stringValue.split(/\s+/); | ||
return Utils.contains(relValues, 'noreferrer') && Utils.contains(relValues, 'noopener'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
/// <reference path="../typings/mocha.d.ts" /> | ||
/// <reference path="../typings/chai.d.ts" /> | ||
|
||
import {TestHelper} from './TestHelper'; | ||
|
||
/** | ||
* Unit tests. | ||
*/ | ||
describe('reactAnchorBlankNoopenerRule', () : void => { | ||
|
||
const ruleName : string = 'react-anchor-blank-noopener'; | ||
|
||
it('should pass on blank anchor with noopener and noreferrer', () : void => { | ||
const script : string = ` | ||
import React = require('react'); | ||
const a = <a target="_blank" rel="noopener noreferrer">link</a>; | ||
const b = <a target="_blank" rel="noreferrer noopener">link</a>; | ||
const c = <a target="_blank" rel="whatever noopener noreferrer">link</a>; | ||
const d = <a target="_blank" rel="noreferrer whatever noopener">link</a>; | ||
const e = <a target="_blank" rel="noreferrer noopener whatever">link</a>; | ||
const f = <a target="_blank" rel="noopener noreferrer"/>; | ||
const g = <a target="_blank" rel="noreferrer noopener"/>; | ||
const h = <a target="_blank" rel="whatever noopener noreferrer"/>; | ||
const i = <a target="_blank" rel="noreferrer whatever noopener"/>; | ||
const j = <a target="_blank" rel="noreferrer noopener whatever"/>; | ||
const k = <a target={ something() } rel="noreferrer noopener whatever"/>; | ||
const l = <a target="_blank" rel={ something() }/>; | ||
`; | ||
|
||
TestHelper.assertViolations(ruleName, script, [ ]); | ||
}); | ||
|
||
it('should pass on anchors without blank', () : void => { | ||
const script : string = ` | ||
import React = require('react'); | ||
const a = <a target="_self" >link</a>; | ||
const b = <a target="_whatever" >link</a>; | ||
const c = <a target="_self" />; | ||
const d = <a target="_whatever" />; | ||
`; | ||
|
||
TestHelper.assertViolations(ruleName, script, [ ]); | ||
}); | ||
|
||
it('should fail on missing rel', () : void => { | ||
const script : string = ` | ||
import React = require('react'); | ||
const a = <a target="_blank">link</a>; | ||
const b = <a target={"_blank"}>link</a>; | ||
`; | ||
|
||
TestHelper.assertViolations(ruleName, script, [ { | ||
"failure": "Anchor tags with target=\"_blank\" should also include rel=\"noopener noreferrer\"", | ||
"name": "file.tsx", | ||
"ruleName": "react-anchor-blank-noopener", | ||
"startPosition": { "character": 23, "line": 4 } | ||
}, | ||
{ | ||
"failure": "Anchor tags with target=\"_blank\" should also include rel=\"noopener noreferrer\"", | ||
"name": "file.tsx", | ||
"ruleName": "react-anchor-blank-noopener", | ||
"startPosition": { "character": 23, "line": 5 } | ||
} | ||
]); | ||
}); | ||
|
||
it('should fail on missing rel - self-closing', () : void => { | ||
const script : string = ` | ||
import React = require('react'); | ||
const a = <a target="_blank" />; | ||
const b = <a target={"_blank"} />; | ||
`; | ||
|
||
TestHelper.assertViolations(ruleName, script, [ | ||
{ | ||
"failure": "Anchor tags with target=\"_blank\" should also include rel=\"noopener noreferrer\"", | ||
"name": "file.tsx", | ||
"ruleName": "react-anchor-blank-noopener", | ||
"startPosition": { "character": 23, "line": 4 } | ||
}, | ||
{ | ||
"failure": "Anchor tags with target=\"_blank\" should also include rel=\"noopener noreferrer\"", | ||
"name": "file.tsx", | ||
"ruleName": "react-anchor-blank-noopener", | ||
"startPosition": { "character": 23, "line": 5 } | ||
} | ||
]); | ||
}); | ||
|
||
it('should fail on empty rel', () : void => { | ||
const script : string = ` | ||
import React = require('react'); | ||
const a = <a target="_blank" rel="" >link</a>; | ||
const b = <a target={"_blank"} rel={""} >link</a>; | ||
`; | ||
|
||
TestHelper.assertViolations(ruleName, script, [ | ||
{ | ||
"failure": "Anchor tags with target=\"_blank\" should also include rel=\"noopener noreferrer\"", | ||
"name": "file.tsx", | ||
"ruleName": "react-anchor-blank-noopener", | ||
"startPosition": { "character": 23, "line": 4 } | ||
}, | ||
{ | ||
"failure": "Anchor tags with target=\"_blank\" should also include rel=\"noopener noreferrer\"", | ||
"name": "file.tsx", | ||
"ruleName": "react-anchor-blank-noopener", | ||
"startPosition": { "character": 23, "line": 5 } | ||
} | ||
]); | ||
}); | ||
|
||
it('should fail on rel with only one term', () : void => { | ||
const script : string = ` | ||
import React = require('react'); | ||
const a = <a target="_blank" rel="noreferrer" >link</a>; | ||
const b = <a target={"_blank"} rel={"noopener"} >link</a>; | ||
`; | ||
|
||
TestHelper.assertViolations(ruleName, script, [ | ||
{ | ||
"failure": "Anchor tags with target=\"_blank\" should also include rel=\"noopener noreferrer\"", | ||
"name": "file.tsx", | ||
"ruleName": "react-anchor-blank-noopener", | ||
"startPosition": { "character": 23, "line": 4 } | ||
}, | ||
{ | ||
"failure": "Anchor tags with target=\"_blank\" should also include rel=\"noopener noreferrer\"", | ||
"name": "file.tsx", | ||
"ruleName": "react-anchor-blank-noopener", | ||
"startPosition": { "character": 23, "line": 5 } | ||
} | ||
]); | ||
}); | ||
|
||
it('should fail on rel with only one term but other terms as well', () : void => { | ||
const script : string = ` | ||
import React = require('react'); | ||
const a = <a target="_blank" rel="noreferrer whatever" >link</a>; | ||
const b = <a target={"_blank"} rel={"whatever noopener"} >link</a>; | ||
`; | ||
|
||
TestHelper.assertViolations(ruleName, script, [ | ||
{ | ||
"failure": "Anchor tags with target=\"_blank\" should also include rel=\"noopener noreferrer\"", | ||
"name": "file.tsx", | ||
"ruleName": "react-anchor-blank-noopener", | ||
"startPosition": { "character": 23, "line": 4 } | ||
}, | ||
{ | ||
"failure": "Anchor tags with target=\"_blank\" should also include rel=\"noopener noreferrer\"", | ||
"name": "file.tsx", | ||
"ruleName": "react-anchor-blank-noopener", | ||
"startPosition": { "character": 23, "line": 5 } | ||
} | ||
]); | ||
}); | ||
|
||
it('should fail on rel with only one term but other terms as well - self closing', () : void => { | ||
const script : string = ` | ||
import React = require('react'); | ||
const a = <a target="_blank" rel="noreferrer whatever" />; | ||
const b = <a target={"_blank"} rel={"whatever noopener"} />; | ||
`; | ||
|
||
TestHelper.assertViolations(ruleName, script, [ | ||
{ | ||
"failure": "Anchor tags with target=\"_blank\" should also include rel=\"noopener noreferrer\"", | ||
"name": "file.tsx", | ||
"ruleName": "react-anchor-blank-noopener", | ||
"startPosition": { "character": 23, "line": 4 } | ||
}, | ||
{ | ||
"failure": "Anchor tags with target=\"_blank\" should also include rel=\"noopener noreferrer\"", | ||
"name": "file.tsx", | ||
"ruleName": "react-anchor-blank-noopener", | ||
"startPosition": { "character": 23, "line": 5 } | ||
} | ||
]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters