Skip to content

Commit

Permalink
Converted react-no-dangerous-html to use a walk function. (microsoft#721
Browse files Browse the repository at this point in the history
)

* Converted react-no-dangerous-html to use a walk function.

* Replaced getExceptions() with a parseOptions() function.
  • Loading branch information
reduckted authored and apawast committed Feb 26, 2019
1 parent 8a6afa9 commit b9fb065
Showing 1 changed file with 52 additions and 57 deletions.
109 changes: 52 additions & 57 deletions src/reactNoDangerousHtmlRule.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as ts from 'typescript';
import { Utils } from './utils/Utils';
import * as Lint from 'tslint';
import * as tsutils from 'tsutils';

import { ExtendedMetadata } from './utils/ExtendedMetadata';

Expand All @@ -10,6 +11,10 @@ export interface Exception {
comment: string;
}

interface Options {
exceptions?: Exception[];
}

export class Rule extends Lint.Rules.AbstractRule {
public static metadata: ExtendedMetadata = {
ruleName: 'react-no-dangerous-html',
Expand All @@ -27,97 +32,60 @@ export class Rule extends Lint.Rules.AbstractRule {
};

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new NoDangerousHtmlWalker(sourceFile, this.getOptions()));
return this.applyWithFunction(sourceFile, walk, this.parseOptions(this.getOptions()));
}

/**
* Exposed for testing.
*/
/* tslint:disable:function-name */
public static getExceptions(options: Lint.IOptions): Exception[] | undefined {
/* tslint:enable:function-name */
private parseOptions(options: Lint.IOptions): Options {
const parsed: Options = {};

if (options.ruleArguments instanceof Array) {
return options.ruleArguments[0];
}
if (options instanceof Array) {
return options;
parsed.exceptions = options.ruleArguments[0];
} else if (options instanceof Array) {
parsed.exceptions = options;
}
return undefined;
return parsed;
}
}

class NoDangerousHtmlWalker extends Lint.RuleWalker {
private currentMethodName: string;

constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) {
super(sourceFile, options);
this.currentMethodName = '<unknown>';
}

protected visitMethodDeclaration(node: ts.MethodDeclaration): void {
this.currentMethodName = node.name.getText();
super.visitMethodDeclaration(node);
this.currentMethodName = '<unknown>';
}

protected visitPropertyAssignment(node: ts.PropertyAssignment): void {
const keyNode: ts.DeclarationName = node.name;
function walk(ctx: Lint.WalkContext<Options>) {
let currentMethodName: string = '<unknown>';

if (keyNode.kind === ts.SyntaxKind.Identifier) {
if (keyNode.text === 'dangerouslySetInnerHTML') {
this.addFailureIfNotSuppressed(node, <ts.Identifier>keyNode);
}
}
super.visitPropertyAssignment(node);
}

protected visitJsxElement(node: ts.JsxElement): void {
this.handleJsxOpeningElement(node.openingElement);
super.visitJsxElement(node);
}

protected visitJsxSelfClosingElement(node: ts.JsxSelfClosingElement): void {
this.handleJsxOpeningElement(node);
super.visitJsxSelfClosingElement(node);
}

private handleJsxOpeningElement(node: ts.JsxOpeningLikeElement): void {
function handleJsxOpeningElement(node: ts.JsxOpeningLikeElement): void {
node.attributes.properties.forEach(
(attribute: ts.JsxAttribute | ts.JsxSpreadAttribute): void => {
if (attribute.kind === ts.SyntaxKind.JsxAttribute) {
const jsxAttribute: ts.JsxAttribute = <ts.JsxAttribute>attribute;
const attributeName = jsxAttribute.name.text;
if (attributeName === 'dangerouslySetInnerHTML') {
this.addFailureIfNotSuppressed(node, <ts.Identifier>jsxAttribute.name);
addFailureIfNotSuppressed(node, <ts.Identifier>jsxAttribute.name);
}
}
}
);
}

private addFailureIfNotSuppressed(parent: ts.Node, node: { text: string }): void {
if (!this.isSuppressed(this.currentMethodName)) {
function addFailureIfNotSuppressed(parent: ts.Node, node: { text: string }): void {
if (!isSuppressed(currentMethodName)) {
const failureString =
'Invalid call to dangerouslySetInnerHTML in method "' +
this.currentMethodName +
currentMethodName +
'".\n' +
' Do *NOT* add a suppression for this warning. If you absolutely must use this API then you need\n' +
' to review the usage with a security expert/QE representative. If they decide that this is an\n' +
' acceptable usage then add the exception to xss_exceptions.json';
const position = parent.getStart();
this.addFailureAt(position, node.text.length, failureString);
ctx.addFailureAt(position, node.text.length, failureString);
}
}

private isSuppressed(methodName: string): boolean {
const exceptions = Rule.getExceptions(this.getOptions());
if (exceptions === undefined || exceptions.length === 0) {
function isSuppressed(methodName: string): boolean {
if (ctx.options.exceptions === undefined || ctx.options.exceptions.length === 0) {
return false; // no file specified means the usage is not suppressed
}
let found = false;
exceptions.forEach(
ctx.options.exceptions.forEach(
(exception: Exception): void => {
if (Utils.absolutePath(exception.file) === this.getSourceFile().fileName) {
if (Utils.absolutePath(exception.file) === ctx.sourceFile.fileName) {
if (exception.method === methodName) {
if (exception.comment !== undefined) {
found = true;
Expand All @@ -128,4 +96,31 @@ class NoDangerousHtmlWalker extends Lint.RuleWalker {
);
return found;
}

function cb(node: ts.Node): void {
if (tsutils.isMethodDeclaration(node)) {
currentMethodName = node.name.getText();
ts.forEachChild(node, cb);
currentMethodName = '<unknown>';
return;
}

if (tsutils.isPropertyAssignment(node)) {
const keyNode: ts.DeclarationName = node.name;

if (keyNode.kind === ts.SyntaxKind.Identifier) {
if (keyNode.text === 'dangerouslySetInnerHTML') {
addFailureIfNotSuppressed(node, <ts.Identifier>keyNode);
}
}
} else if (tsutils.isJsxElement(node)) {
handleJsxOpeningElement(node.openingElement);
} else if (tsutils.isJsxSelfClosingElement(node)) {
handleJsxOpeningElement(node);
}

return ts.forEachChild(node, cb);
}

return ts.forEachChild(ctx.sourceFile, cb);
}

0 comments on commit b9fb065

Please sign in to comment.