import { Allowlist } from '../../third_party/tsetse/allowlist' ; import { Checker } from '../../third_party/tsetse/checker' ; import { ErrorCode } from '../../third_party/tsetse/error_code' ; import { AbstractRule } from '../../third_party/tsetse/rule' ; import { shouldExamineNode } from '../../third_party/tsetse/util/ast_tools' ; import { isLiteral } from '../../third_party/tsetse/util/is_literal' ; import { PropertyMatcher } from '../../third_party/tsetse/util/property_matcher' ; import * as ts from 'typescript' ; import { RuleConfiguration } from '../../rule_configuration' ; let errMsg = 'Do not use Element#setAttribute or similar APIs, as this can lead to XSS.' ; const BANNED_APIS = [ 'Element.prototype.setAttribute' , 'Element.prototype.setAttributeNS' , 'Element.prototype.setAttributeNode' , 'Element.prototype.setAttributeNodeNS' , ] ; /**
 * Security sensitive attribute names that should not be set through
 * `setAttribute` or similar functions.
 */ export const SECURITY_SENSITIVE_ATTRIBUTES = new Set ( [ 'src' , 'srcdoc' , ] ) ; function matchNode ( tc , n , matcher ) { if ( ! shouldExamineNode ( n ) ) return ; if ( ! matcher . typeMatches ( tc . getTypeAtLocation ( n . expression ) ) ) return ; if ( ! ts . isCallExpression ( n . parent ) ) return n ; if ( n . parent . expression !== n ) return n ; if ( matcher . bannedProperty === 'setAttribute' ) { if ( n . parent . arguments . length !== 2 ) return ; const ty = tc . getTypeAtLocation ( n . parent . arguments [ 0 ] ) ; if ( ty . isStringLiteral ( ) && ! SECURITY_SENSITIVE_ATTRIBUTES . has ( ty . value . toLowerCase ( ) ) && isLiteral ( tc , n . parent . arguments [ 0 ] ) ) { return ; } } return n ; } /** A Rule that looks for use of Element#setAttribute and similar properties. */ export class Rule extends AbstractRule { static readonly RULE_NAME = 'ban-element-setattribute' ; readonly ruleName = Rule . RULE_NAME ; readonly code = ErrorCode . CONFORMANCE_PATTERN ; private readonly propMatchers : readonly PropertyMatcher [ ] ; private readonly allowlist ? : Allowlist ; constructor ( configuration = { } ) { super ( ) ; this . propMatchers = BANNED_APIS . map ( PropertyMatcher . fromSpec ) ; if ( configuration . allowlistEntries ) { this . allowlist = new Allowlist ( configuration . allowlistEntries ) ; } } register ( checker ) { for ( const matcher of this . propMatchers ) { checker . onNamedPropertyAccess ( matcher . bannedProperty , ( c , n ) => { const node = matchNode ( c . typeChecker , n , matcher ) ; if ( node ) { checker . addFailureAtNode ( node , errMsg , Rule . RULE_NAME , this . allowlist ) ; } } , this . code ) ; checker . onStringLiteralElementAccess ( matcher . bannedProperty , ( c , n ) => { const node = matchNode ( c . typeChecker , n , matcher ) ; if ( node ) { checker . addFailureAtNode ( node , errMsg , Rule . RULE_NAME , this . allowlist ) ; } } , this . code ) ; } } }