├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── common ├── configured_checker.ts ├── exemption_config.ts ├── rule_configuration.ts ├── rule_groups.ts ├── rules │ ├── dom_security │ │ ├── ban_base_href_assignments.ts │ │ ├── ban_document_execcommand.ts │ │ ├── ban_document_write_calls.ts │ │ ├── ban_document_writeln_calls.ts │ │ ├── ban_domparser_parsefromstring.ts │ │ ├── ban_element_innerhtml_assignments.ts │ │ ├── ban_element_insertadjacenthtml.ts │ │ ├── ban_element_outerhtml_assignments.ts │ │ ├── ban_element_setattribute.ts │ │ ├── ban_eval_calls.ts │ │ ├── ban_function_calls.ts │ │ ├── ban_iframe_srcdoc_assignments.ts │ │ ├── ban_object_data_assignments.ts │ │ ├── ban_range_createcontextualfragment.ts │ │ ├── ban_script_appendchild_calls.ts │ │ ├── ban_script_content_assignments.ts │ │ ├── ban_script_src_assignments.ts │ │ ├── ban_serviceworkercontainer_register.ts │ │ ├── ban_shared_worker_calls.ts │ │ ├── ban_trustedtypes_createpolicy.ts │ │ ├── ban_window_stringfunctiondef.ts │ │ ├── ban_worker_calls.ts │ │ └── ban_worker_importscripts.ts │ └── unsafe │ │ ├── ban_legacy_conversions.ts │ │ └── ban_reviewed_conversions.ts ├── third_party │ └── tsetse │ │ ├── allowlist.ts │ │ ├── checker.ts │ │ ├── error_code.ts │ │ ├── failure.ts │ │ ├── rule.ts │ │ ├── rules │ │ ├── check_side_effect_import_rule.ts │ │ └── conformance_pattern_rule.ts │ │ └── util │ │ ├── absolute_matcher.ts │ │ ├── ast_tools.ts │ │ ├── ban_jsdoc.ts │ │ ├── fixer.ts │ │ ├── is_expression_value_used_or_void.ts │ │ ├── is_literal.ts │ │ ├── is_trusted_type.ts │ │ ├── pattern_config.ts │ │ ├── pattern_engines │ │ ├── name_engine.ts │ │ ├── pattern_engine.ts │ │ ├── property_engine.ts │ │ ├── property_non_constant_write_engine.ts │ │ └── property_write_engine.ts │ │ ├── property_matcher.ts │ │ └── trusted_types_configuration.ts └── tsconfig.json ├── docs └── supported-checks.md ├── jasmine.json ├── package.json ├── packages ├── eslint_plugin_tsec │ ├── index.ts │ ├── package.json │ ├── trusted_types_checks.ts │ └── tsconfig.json └── tsec │ ├── .npmignore │ ├── bin │ └── tsec │ ├── build.ts │ ├── compiler_host.ts │ ├── index.bzl │ ├── language_service_plugin.ts │ ├── package.json │ ├── report.ts │ ├── tsconfig.json │ ├── tsec.ts │ └── utils.ts ├── test ├── asset │ ├── dom_security │ │ ├── ban_base_href_assignments │ │ │ └── base_href_writes.ts │ │ ├── ban_document_execcommand │ │ │ └── document_execcommand.ts │ │ ├── ban_document_write_calls │ │ │ └── document_write_calls.ts │ │ ├── ban_document_writeln_calls │ │ │ └── document_writeln_calls.ts │ │ ├── ban_domparser_parsefromstring │ │ │ └── parse_calls.ts │ │ ├── ban_element_innerhtml_assignments │ │ │ ├── element_innerhtml_assignments.ts │ │ │ └── element_innerhtml_assignments_tt_awareness.ts │ │ ├── ban_element_insertadjacenthtml │ │ │ └── element_insertadjacenthtml.ts │ │ ├── ban_element_outerhtml_assignments │ │ │ └── element_outerhtml_assignments.ts │ │ ├── ban_element_setattribute │ │ │ └── element_setattribute.ts │ │ ├── ban_eval_calls │ │ │ ├── eval_calls.ts │ │ │ └── eval_calls_tt_awareness.ts │ │ ├── ban_function_calls │ │ │ ├── function.ts │ │ │ └── function_tt_awareness.ts │ │ ├── ban_iframe_srcdoc_assignments │ │ │ └── iframe_srcdoc_assignments.ts │ │ ├── ban_object_data_assignments │ │ │ └── object_data_writes.ts │ │ ├── ban_range_createcontextualfragment │ │ │ └── create_contextual_fragment.ts │ │ ├── ban_script_appendchild_calls │ │ │ └── script_appendchild_calls.ts │ │ ├── ban_script_content_assignments │ │ │ └── script_content_writes.ts │ │ ├── ban_script_src_assignments │ │ │ └── script_src_writes.ts │ │ ├── ban_serviceworkercontainer_register │ │ │ └── scriptworkercontainer_register.ts │ │ ├── ban_shared_worker_calls │ │ │ └── shared_worker_calls.ts │ │ ├── ban_trustedtypes_createpolicy │ │ │ └── createpolicy_calls.ts │ │ ├── ban_window_stringfunctiondef │ │ │ ├── stringfunctiondef_calls.ts │ │ │ └── stringfunctiondef_calls_tt_awareness.ts │ │ ├── ban_worker_calls │ │ │ └── worker_calls.ts │ │ └── ban_worker_importscripts │ │ │ ├── importscripts.ts │ │ │ └── importscripts_tt_awareness.ts │ └── unsafe │ │ ├── ban_legacy_conversions │ │ └── legacy_conversions_calls.ts │ │ └── ban_reviewed_conversions │ │ └── unchecked_conversions_calls.ts ├── golden.json ├── harness.ts └── spec.ts ├── tsconfig-compile.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // see: https://code.visualstudio.com/docs/editor/debugging 2 | { 3 | // Use IntelliSense to learn about possible attributes. 4 | // Hover to view descriptions of existing attributes. 5 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "type": "node", 10 | "request": "launch", 11 | "name": "Debug tsec", 12 | "skipFiles": ["/**"], 13 | "program": "${workspaceFolder}/packages/tsec/tsec.ts", 14 | "preLaunchTask": "tsc: build - tsconfig.json", 15 | "outFiles": ["${workspaceFolder}/packages/tsec/lib/**/*.js"] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "typescript", 6 | "tsconfig": "tsconfig.json", 7 | "problemMatcher": ["$tsc"], 8 | "group": "build", 9 | "label": "tsc: build - tsconfig.json" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We currently do not accept pull requests. However, feature requests and any 4 | other feedbacks are welcome! 5 | 6 | ## Community Guidelines 7 | 8 | This project follows 9 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 10 | -------------------------------------------------------------------------------- /common/configured_checker.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {ENABLED_RULES} from './rule_groups'; 16 | import {Checker} from './third_party/tsetse/checker'; 17 | import * as ts from 'typescript'; 18 | 19 | import { 20 | ExemptionList, 21 | parseExemptionConfig, 22 | resolveExemptionConfigPath, 23 | } from './exemption_config'; 24 | 25 | /** 26 | * Create a new cheker with all enabled rules registered and the exemption list 27 | * configured. 28 | */ 29 | export function getConfiguredChecker( 30 | program: ts.Program, 31 | host: ts.ModuleResolutionHost, 32 | ): {checker: Checker; errors: ts.Diagnostic[]} { 33 | let exemptionList: ExemptionList | undefined = undefined; 34 | 35 | const exemptionConfigPath = resolveExemptionConfigPath( 36 | program.getCompilerOptions()['configFilePath'] as string, 37 | ); 38 | 39 | const errors = []; 40 | 41 | if (exemptionConfigPath) { 42 | const projExemptionConfigOrErr = parseExemptionConfig(exemptionConfigPath); 43 | if (projExemptionConfigOrErr instanceof ExemptionList) { 44 | exemptionList = projExemptionConfigOrErr; 45 | } else { 46 | errors.push(...projExemptionConfigOrErr); 47 | } 48 | } 49 | 50 | // Create all enabled rules with corresponding exemption list entries. 51 | const checker = new Checker(program, host); 52 | const wildcardAllowListEntry = exemptionList?.get('*'); 53 | const rules = ENABLED_RULES.map((ruleCtr) => { 54 | const allowlistEntries = []; 55 | const allowlistEntry = exemptionList?.get(ruleCtr.RULE_NAME); 56 | if (allowlistEntry) { 57 | allowlistEntries.push(allowlistEntry); 58 | } 59 | if (wildcardAllowListEntry) { 60 | allowlistEntries.push(wildcardAllowListEntry); 61 | } 62 | return new ruleCtr({allowlistEntries}); 63 | }); 64 | 65 | // Register all rules. 66 | for (const rule of rules) { 67 | rule.register(checker); 68 | } 69 | 70 | return {checker, errors}; 71 | } 72 | -------------------------------------------------------------------------------- /common/exemption_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import * as glob from 'glob'; 16 | import { 17 | AllowlistEntry, 18 | ExemptionReason, 19 | } from './third_party/tsetse/allowlist'; 20 | import * as minimatch from 'minimatch'; 21 | import * as os from 'os'; 22 | import * as path from 'path'; 23 | import * as ts from 'typescript'; 24 | 25 | /** 26 | * Stores exemption list configurations by rules. Supports commonly used Map 27 | * operations. 28 | */ 29 | export class ExemptionList { 30 | // Extending Map doesn't work in ES5. 31 | private readonly map: Map; 32 | 33 | constructor(copyFrom?: ExemptionList) { 34 | this.map = new Map(copyFrom?.map.entries() ?? []); 35 | } 36 | 37 | get(rule: string): AllowlistEntry | undefined { 38 | return this.map.get(rule); 39 | } 40 | 41 | set(rule: string, allowlistEntry: AllowlistEntry) { 42 | this.map.set(rule, allowlistEntry); 43 | } 44 | 45 | entries() { 46 | return this.map.entries(); 47 | } 48 | 49 | get size() { 50 | return this.map.size; 51 | } 52 | } 53 | 54 | /** Get the path of the exemption configuration file from compiler options. */ 55 | export function resolveExemptionConfigPath( 56 | configFilePath: string, 57 | ): string | undefined { 58 | if (!ts.sys.fileExists(configFilePath)) { 59 | configFilePath += ts.Extension.Json; 60 | if (!ts.sys.fileExists(configFilePath)) { 61 | return undefined; 62 | } 63 | } 64 | 65 | const {config} = ts.readConfigFile(configFilePath, ts.sys.readFile); 66 | const options = config?.compilerOptions; 67 | 68 | const configFileDir = path.dirname(configFilePath); 69 | 70 | if (Array.isArray(options?.plugins)) { 71 | for (const plugin of options.plugins as ts.PluginImport[]) { 72 | if (plugin.name !== 'tsec') continue; 73 | const {exemptionConfig} = plugin as {exemptionConfig?: unknown}; 74 | if (typeof exemptionConfig === 'string') { 75 | // Path of the exemption config is relative to the path of 76 | // tsconfig.json. Resolve it to the absolute path. 77 | const resolvedPath = path.resolve(configFileDir, exemptionConfig); 78 | // Always returned a path to an existing file so that tsec won't crash. 79 | if (ts.sys.fileExists(resolvedPath)) { 80 | return resolvedPath; 81 | } 82 | } 83 | } 84 | } 85 | 86 | if (typeof config.extends === 'string') { 87 | return resolveExemptionConfigPath( 88 | path.resolve(configFileDir, config.extends), 89 | ); 90 | } 91 | 92 | return undefined; 93 | } 94 | 95 | /** Create a Diagnostic for a JSON node from a configuration file */ 96 | function getDiagnosticErrorFromJsonNode( 97 | node: ts.Node, 98 | file: ts.JsonSourceFile, 99 | messageText: string, 100 | ): ts.Diagnostic { 101 | const start = node.getStart(file); 102 | const length = node.getEnd() - start; 103 | return { 104 | source: 'tsec', 105 | category: ts.DiagnosticCategory.Error, 106 | code: 21110, 107 | file, 108 | start, 109 | length, 110 | messageText, 111 | }; 112 | } 113 | 114 | /** Parse the content of the exemption configuration file. */ 115 | export function parseExemptionConfig( 116 | exemptionConfigPath: string, 117 | ): ExemptionList | ts.Diagnostic[] { 118 | const errors: ts.Diagnostic[] = []; 119 | 120 | const jsonContent = ts.sys.readFile(exemptionConfigPath)!; 121 | const jsonSourceFile = ts.parseJsonText(exemptionConfigPath, jsonContent); 122 | 123 | if (!jsonSourceFile.statements.length) { 124 | errors.push({ 125 | source: 'tsec', 126 | category: ts.DiagnosticCategory.Error, 127 | code: 21110, 128 | file: jsonSourceFile, 129 | start: 1, 130 | length: undefined, 131 | messageText: 'Invalid exemtpion list', 132 | }); 133 | return errors; 134 | } 135 | 136 | const jsonObj = jsonSourceFile.statements[0].expression; 137 | if (!ts.isObjectLiteralExpression(jsonObj)) { 138 | errors.push( 139 | getDiagnosticErrorFromJsonNode( 140 | jsonObj, 141 | jsonSourceFile, 142 | 'Exemption configuration requires a value of type object', 143 | ), 144 | ); 145 | return errors; 146 | } 147 | 148 | const exemption = new ExemptionList(); 149 | const baseDir = path.dirname(exemptionConfigPath); 150 | const globOptions = {cwd: baseDir, absolute: true, silent: true}; 151 | const isWin = os.platform() === 'win32'; 152 | 153 | for (const prop of jsonObj.properties) { 154 | if (!ts.isPropertyAssignment(prop)) { 155 | errors.push( 156 | getDiagnosticErrorFromJsonNode( 157 | prop, 158 | jsonSourceFile, 159 | 'Property assignment expected', 160 | ), 161 | ); 162 | continue; 163 | } 164 | 165 | if (prop.name === undefined) continue; 166 | 167 | if ( 168 | !ts.isStringLiteral(prop.name) || 169 | !prop.name.getText(jsonSourceFile).startsWith(`"`) 170 | ) { 171 | errors.push( 172 | getDiagnosticErrorFromJsonNode( 173 | prop.name, 174 | jsonSourceFile, 175 | 'String literal with double quotes expected', 176 | ), 177 | ); 178 | continue; 179 | } 180 | 181 | const ruleName = prop.name.text; 182 | 183 | if (!ts.isArrayLiteralExpression(prop.initializer)) { 184 | errors.push( 185 | getDiagnosticErrorFromJsonNode( 186 | prop.initializer, 187 | jsonSourceFile, 188 | `Exemption entry '${ruleName}' requires a value of type Array`, 189 | ), 190 | ); 191 | continue; 192 | } 193 | 194 | const fileNames: string[] = []; 195 | const patterns: string[] = []; 196 | 197 | for (const elem of prop.initializer.elements) { 198 | if (!ts.isStringLiteral(elem)) { 199 | errors.push( 200 | getDiagnosticErrorFromJsonNode( 201 | elem, 202 | jsonSourceFile, 203 | `Item of exemption entry '${ruleName}' requires values of type string`, 204 | ), 205 | ); 206 | continue; 207 | } 208 | let pathLike = path.resolve(baseDir, elem.text); 209 | if (isWin) { 210 | pathLike = pathLike.replace(/\\/g, '/'); 211 | } 212 | if (glob.hasMagic(elem.text, globOptions)) { 213 | patterns.push( 214 | // Strip the leading and trailing '/' from the stringified regexp. 215 | minimatch.makeRe(pathLike, {}).toString().slice(1, -1), 216 | ); 217 | } else { 218 | fileNames.push(pathLike); 219 | } 220 | } 221 | 222 | exemption.set(ruleName, { 223 | reason: ExemptionReason.UNSPECIFIED, 224 | path: fileNames, 225 | regexp: patterns, 226 | }); 227 | } 228 | 229 | return errors.length > 0 ? errors : exemption; 230 | } 231 | -------------------------------------------------------------------------------- /common/rule_configuration.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {AllowlistEntry} from './third_party/tsetse/allowlist'; 16 | 17 | /** 18 | * A configuration interface passed to the rules with properties configured 19 | * externally either via allowlist or bootstrap file. 20 | */ 21 | export interface RuleConfiguration { 22 | /** A list of allowlist blocks. */ 23 | allowlistEntries?: AllowlistEntry[]; 24 | } 25 | -------------------------------------------------------------------------------- /common/rule_groups.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {AbstractRule} from './third_party/tsetse/rule'; 16 | 17 | import {RuleConfiguration} from './rule_configuration'; 18 | import {Rule as TTBanBaseHrefAssignments} from './rules/dom_security/ban_base_href_assignments'; 19 | import {Rule as TTBanDocumentExecCommand} from './rules/dom_security/ban_document_execcommand'; 20 | import {Rule as TTBanDocumentWriteCalls} from './rules/dom_security/ban_document_write_calls'; 21 | import {Rule as TTBanDocumentWritelnCalls} from './rules/dom_security/ban_document_writeln_calls'; 22 | import {Rule as TTBanDomParserParseFromString} from './rules/dom_security/ban_domparser_parsefromstring'; 23 | import {Rule as TTBanElementInnerHTMLAssignments} from './rules/dom_security/ban_element_innerhtml_assignments'; 24 | import {Rule as TTBanElementInsertAdjacentHTML} from './rules/dom_security/ban_element_insertadjacenthtml'; 25 | import {Rule as TTBanElementOuterHTMLAssignments} from './rules/dom_security/ban_element_outerhtml_assignments'; 26 | import {Rule as TTBanElementSetAttribute} from './rules/dom_security/ban_element_setattribute'; 27 | import {Rule as TTBanEvalCalls} from './rules/dom_security/ban_eval_calls'; 28 | import {Rule as TTBanFunctionCalls} from './rules/dom_security/ban_function_calls'; 29 | import {Rule as TTBanIFrameSrcdocAssignments} from './rules/dom_security/ban_iframe_srcdoc_assignments'; 30 | import {Rule as TTBanObjectDataAssignments} from './rules/dom_security/ban_object_data_assignments'; 31 | import {Rule as TTBanRangeCreateContextualFragment} from './rules/dom_security/ban_range_createcontextualfragment'; 32 | import {Rule as TTBanScriptAppendChildCalls} from './rules/dom_security/ban_script_appendchild_calls'; 33 | import {Rule as TTBanScriptContentAssignments} from './rules/dom_security/ban_script_content_assignments'; 34 | import {Rule as TTBanScriptSrcAssignments} from './rules/dom_security/ban_script_src_assignments'; 35 | import {Rule as TTBanServiceWorkerContainerRegister} from './rules/dom_security/ban_serviceworkercontainer_register'; 36 | import {Rule as TTBanSharedWorkerCalls} from './rules/dom_security/ban_shared_worker_calls'; 37 | import {Rule as TTBanTrustedTypesCreatepolicy} from './rules/dom_security/ban_trustedtypes_createpolicy'; 38 | import {Rule as TTBanWindowStringfunctiondef} from './rules/dom_security/ban_window_stringfunctiondef'; 39 | import {Rule as TTBanWorkerCalls} from './rules/dom_security/ban_worker_calls'; 40 | import {Rule as TTBanWorkerImportScripts} from './rules/dom_security/ban_worker_importscripts'; 41 | import {Rule as BanLegacyConversions} from './rules/unsafe/ban_legacy_conversions'; 42 | import {Rule as BanUncheckedConversions} from './rules/unsafe/ban_reviewed_conversions'; 43 | 44 | /** 45 | * An interface unifying rules extending `AbstractRule` and those extending 46 | * `ConfornacePatternRule`. The interface exposes rule names and make it 47 | * possible to configure non-Bazel exemption list during rule creation. 48 | */ 49 | export interface RuleConstructor { 50 | readonly RULE_NAME: string; 51 | new (configuration?: RuleConfiguration): AbstractRule; 52 | } 53 | 54 | /** Conformance rules related to Trusted Types adoption */ 55 | export const TRUSTED_TYPES_RELATED_RULES: readonly RuleConstructor[] = [ 56 | TTBanBaseHrefAssignments, // https://github.com/w3c/webappsec-trusted-types/issues/172 57 | TTBanDocumentExecCommand, 58 | TTBanDocumentWritelnCalls, 59 | TTBanDocumentWriteCalls, 60 | TTBanEvalCalls, 61 | TTBanFunctionCalls, 62 | TTBanIFrameSrcdocAssignments, 63 | TTBanObjectDataAssignments, 64 | TTBanScriptAppendChildCalls, 65 | TTBanScriptContentAssignments, 66 | TTBanScriptSrcAssignments, 67 | TTBanServiceWorkerContainerRegister, 68 | TTBanSharedWorkerCalls, 69 | TTBanTrustedTypesCreatepolicy, 70 | TTBanWindowStringfunctiondef, 71 | TTBanWorkerCalls, 72 | TTBanWorkerImportScripts, 73 | TTBanElementOuterHTMLAssignments, 74 | TTBanElementInnerHTMLAssignments, 75 | TTBanElementInsertAdjacentHTML, 76 | TTBanDomParserParseFromString, 77 | TTBanElementSetAttribute, 78 | TTBanRangeCreateContextualFragment, 79 | BanLegacyConversions, 80 | BanUncheckedConversions, 81 | ]; 82 | 83 | /** 84 | * Conformance rules that should be registered by the check as a compiler 85 | * plugin. 86 | */ 87 | export const ENABLED_RULES: readonly RuleConstructor[] = [ 88 | ...TRUSTED_TYPES_RELATED_RULES, 89 | ]; 90 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_base_href_assignments.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | import {RuleConfiguration} from '../../rule_configuration'; 21 | 22 | let errMsg = 23 | 'Do not modify HTMLBaseElement#href elements, as this can compromise all other efforts to sanitize unsafe URLs and lead to XSS.'; 24 | 25 | /** 26 | * A Rule that looks for dynamic assignments to HTMLBaseElement#href property. 27 | * With this property modified, every URL in the page becomes unsafe. 28 | * Developers should avoid writing to this property. 29 | */ 30 | export class Rule extends ConformancePatternRule { 31 | static readonly RULE_NAME = 'ban-base-href-assignments'; 32 | 33 | constructor(configuration: RuleConfiguration = {}) { 34 | super({ 35 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 36 | errorMessage: errMsg, 37 | kind: PatternKind.BANNED_PROPERTY_WRITE, 38 | values: ['HTMLBaseElement.prototype.href'], 39 | name: Rule.RULE_NAME, 40 | ...configuration, 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_document_execcommand.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {Allowlist} from '../../third_party/tsetse/allowlist'; 16 | import {Checker} from '../../third_party/tsetse/checker'; 17 | import {ErrorCode} from '../../third_party/tsetse/error_code'; 18 | import {AbstractRule} from '../../third_party/tsetse/rule'; 19 | import {shouldExamineNode} from '../../third_party/tsetse/util/ast_tools'; 20 | import {isLiteral} from '../../third_party/tsetse/util/is_literal'; 21 | import {PropertyMatcher} from '../../third_party/tsetse/util/property_matcher'; 22 | import * as ts from 'typescript'; 23 | 24 | import {RuleConfiguration} from '../../rule_configuration'; 25 | 26 | let errMsg = 27 | "Do not use document.execCommand('insertHTML'), as this can lead to XSS."; 28 | 29 | function matchNode( 30 | tc: ts.TypeChecker, 31 | n: ts.PropertyAccessExpression | ts.ElementAccessExpression, 32 | matcher: PropertyMatcher, 33 | ) { 34 | if (!shouldExamineNode(n)) return; 35 | if (!matcher.typeMatches(tc.getTypeAtLocation(n.expression))) return; 36 | 37 | // Check if the matched node is a call to `execCommand` and if the command 38 | // name is a literal. We will skip matching if the command name is not in 39 | // the blocklist. 40 | if (!ts.isCallExpression(n.parent)) return; 41 | if (n.parent.expression !== n) return; 42 | // It's OK if someone provided the wrong number of arguments because the code 43 | // will have other compiler errors. 44 | if (n.parent.arguments.length < 1) return; 45 | const ty = tc.getTypeAtLocation(n.parent.arguments[0]); 46 | if ( 47 | ty.isStringLiteral() && 48 | ty.value.toLowerCase() !== 'inserthtml' && 49 | isLiteral(tc, n.parent.arguments[0]) 50 | ) { 51 | return; 52 | } 53 | 54 | return n; 55 | } 56 | 57 | /** A Rule that looks for use of Document#execCommand. */ 58 | export class Rule extends AbstractRule { 59 | static readonly RULE_NAME = 'ban-document-execcommand'; 60 | 61 | readonly ruleName: string = Rule.RULE_NAME; 62 | readonly code: ErrorCode = ErrorCode.CONFORMANCE_PATTERN; 63 | 64 | private readonly propMatcher: PropertyMatcher; 65 | private readonly allowlist?: Allowlist; 66 | 67 | constructor(configuration: RuleConfiguration = {}) { 68 | super(); 69 | this.propMatcher = PropertyMatcher.fromSpec( 70 | 'Document.prototype.execCommand', 71 | ); 72 | if (configuration.allowlistEntries) { 73 | this.allowlist = new Allowlist(configuration.allowlistEntries); 74 | } 75 | } 76 | 77 | register(checker: Checker): void { 78 | checker.onNamedPropertyAccess( 79 | this.propMatcher.bannedProperty, 80 | (c, n) => { 81 | const node = matchNode(c.typeChecker, n, this.propMatcher); 82 | if (node) { 83 | checker.addFailureAtNode( 84 | node, 85 | errMsg, 86 | Rule.RULE_NAME, 87 | this.allowlist, 88 | ); 89 | } 90 | }, 91 | this.code, 92 | ); 93 | 94 | checker.onStringLiteralElementAccess( 95 | this.propMatcher.bannedProperty, 96 | (c, n) => { 97 | const node = matchNode(c.typeChecker, n, this.propMatcher); 98 | if (node) { 99 | checker.addFailureAtNode( 100 | node, 101 | errMsg, 102 | Rule.RULE_NAME, 103 | this.allowlist, 104 | ); 105 | } 106 | }, 107 | this.code, 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_document_write_calls.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; 21 | import {TRUSTED_HTML} from '../../third_party/tsetse/util/trusted_types_configuration'; 22 | 23 | import {RuleConfiguration} from '../../rule_configuration'; 24 | 25 | let errMsg = 'Do not use Document#write, as this can lead to XSS.'; 26 | 27 | /** 28 | * A Rule that looks for use of Document#write properties. 29 | */ 30 | export class Rule extends ConformancePatternRule { 31 | static readonly RULE_NAME = 'ban-document-write-calls'; 32 | 33 | constructor(configuration: RuleConfiguration = {}) { 34 | super( 35 | overridePatternConfig({ 36 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 37 | errorMessage: errMsg, 38 | kind: PatternKind.BANNED_PROPERTY, 39 | values: ['Document.prototype.write'], 40 | name: Rule.RULE_NAME, 41 | allowedTrustedType: TRUSTED_HTML, 42 | ...configuration, 43 | }), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_document_writeln_calls.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | import {TRUSTED_HTML} from '../../third_party/tsetse/util/trusted_types_configuration'; 21 | 22 | import {RuleConfiguration} from '../../rule_configuration'; 23 | 24 | let errMsg = 'Do not use Document#writeln, as this can lead to XSS.'; 25 | 26 | /** 27 | * A Rule that looks for use of Document#writeln properties. 28 | */ 29 | export class Rule extends ConformancePatternRule { 30 | static readonly RULE_NAME = 'ban-document-writeln-calls'; 31 | 32 | constructor(configuration: RuleConfiguration = {}) { 33 | super({ 34 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 35 | errorMessage: errMsg, 36 | kind: PatternKind.BANNED_PROPERTY, 37 | values: ['Document.prototype.writeln'], 38 | name: Rule.RULE_NAME, 39 | allowedTrustedType: TRUSTED_HTML, 40 | ...configuration, 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_domparser_parsefromstring.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; 21 | import {RuleConfiguration} from '../../rule_configuration'; 22 | 23 | let errMsg = 24 | 'Using DOMParser#parseFromString to parse untrusted input into DOM elements can lead to XSS.'; 25 | 26 | /** A rule that bans any use of DOMParser.prototype.parseFromString. */ 27 | export class Rule extends ConformancePatternRule { 28 | static readonly RULE_NAME = 'ban-domparser-parsefromstring'; 29 | 30 | constructor(configuration: RuleConfiguration = {}) { 31 | super( 32 | overridePatternConfig({ 33 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 34 | errorMessage: errMsg, 35 | kind: PatternKind.BANNED_PROPERTY, 36 | values: ['DOMParser.prototype.parseFromString'], 37 | name: Rule.RULE_NAME, 38 | ...configuration, 39 | }), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_element_innerhtml_assignments.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; 21 | import {TRUSTED_HTML} from '../../third_party/tsetse/util/trusted_types_configuration'; 22 | 23 | import {RuleConfiguration} from '../../rule_configuration'; 24 | 25 | let errMsg = 26 | 'Assigning directly to Element#innerHTML can result in XSS vulnerabilities.'; 27 | 28 | /** 29 | * A Rule that looks for assignments to an Element's innerHTML property. 30 | */ 31 | export class Rule extends ConformancePatternRule { 32 | static readonly RULE_NAME = 'ban-element-innerhtml-assignments'; 33 | constructor(configuration: RuleConfiguration = {}) { 34 | super( 35 | overridePatternConfig({ 36 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 37 | errorMessage: errMsg, 38 | kind: PatternKind.BANNED_PROPERTY_WRITE, 39 | values: [ 40 | 'Element.prototype.innerHTML', 41 | 'ShadowRoot.prototype.innerHTML', 42 | ], 43 | name: Rule.RULE_NAME, 44 | allowedTrustedType: TRUSTED_HTML, 45 | ...configuration, 46 | }), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_element_insertadjacenthtml.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; 21 | import {TRUSTED_HTML} from '../../third_party/tsetse/util/trusted_types_configuration'; 22 | 23 | import {RuleConfiguration} from '../../rule_configuration'; 24 | 25 | let errMsg = 'Do not use Element#insertAdjacentHTML, as this can lead to XSS.'; 26 | 27 | /** 28 | * A Rule that looks for use of Element#insertAdjacentHTML properties. 29 | */ 30 | export class Rule extends ConformancePatternRule { 31 | static readonly RULE_NAME = 'ban-element-insertadjacenthtml'; 32 | 33 | constructor(configuration: RuleConfiguration = {}) { 34 | super( 35 | overridePatternConfig({ 36 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 37 | errorMessage: errMsg, 38 | kind: PatternKind.BANNED_PROPERTY, 39 | values: ['Element.prototype.insertAdjacentHTML'], 40 | name: Rule.RULE_NAME, 41 | allowedTrustedType: TRUSTED_HTML, 42 | ...configuration, 43 | }), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_element_outerhtml_assignments.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; 21 | import {TRUSTED_HTML} from '../../third_party/tsetse/util/trusted_types_configuration'; 22 | 23 | import {RuleConfiguration} from '../../rule_configuration'; 24 | 25 | let errMsg = 26 | 'Assigning directly to Element#outerHTML can result in XSS vulnerabilities.'; 27 | 28 | /** 29 | * A Rule that looks for assignments to an Element's innerHTML property. 30 | */ 31 | export class Rule extends ConformancePatternRule { 32 | static readonly RULE_NAME = 'ban-element-outerhtml-assignments'; 33 | constructor(configuration: RuleConfiguration = {}) { 34 | super( 35 | overridePatternConfig({ 36 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 37 | errorMessage: errMsg, 38 | kind: PatternKind.BANNED_PROPERTY_WRITE, 39 | values: ['Element.prototype.outerHTML'], 40 | name: Rule.RULE_NAME, 41 | allowedTrustedType: TRUSTED_HTML, 42 | ...configuration, 43 | }), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_element_setattribute.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {Allowlist} from '../../third_party/tsetse/allowlist'; 16 | import {Checker} from '../../third_party/tsetse/checker'; 17 | import {ErrorCode} from '../../third_party/tsetse/error_code'; 18 | import {AbstractRule} from '../../third_party/tsetse/rule'; 19 | import {shouldExamineNode} from '../../third_party/tsetse/util/ast_tools'; 20 | import {isLiteral} from '../../third_party/tsetse/util/is_literal'; 21 | import {PropertyMatcher} from '../../third_party/tsetse/util/property_matcher'; 22 | import * as ts from 'typescript'; 23 | 24 | import {RuleConfiguration} from '../../rule_configuration'; 25 | 26 | const BANNED_APIS = [ 27 | 'Element.prototype.setAttribute', 28 | 'Element.prototype.setAttributeNS', 29 | 'Element.prototype.setAttributeNode', 30 | 'Element.prototype.setAttributeNodeNS', 31 | ]; 32 | 33 | /** 34 | * Trusted Types related attribute names that should not be set through 35 | * `setAttribute` or similar functions. 36 | */ 37 | export const TT_RELATED_ATTRIBUTES: Set = new Set([ 38 | 'src', 39 | 'srcdoc', 40 | 'data', 41 | 'codebase', 42 | ]); 43 | 44 | /** A Rule that looks for use of Element#setAttribute and similar properties. */ 45 | export abstract class BanSetAttributeRule extends AbstractRule { 46 | readonly code: ErrorCode = ErrorCode.CONFORMANCE_PATTERN; 47 | 48 | private readonly propMatchers: readonly PropertyMatcher[]; 49 | private readonly allowlist?: Allowlist; 50 | 51 | constructor(configuration: RuleConfiguration) { 52 | super(); 53 | this.propMatchers = BANNED_APIS.map(PropertyMatcher.fromSpec); 54 | if (configuration.allowlistEntries) { 55 | this.allowlist = new Allowlist(configuration.allowlistEntries); 56 | } 57 | } 58 | 59 | protected abstract readonly errorMessage: string; 60 | protected abstract readonly isSecuritySensitiveAttrName: ( 61 | attr: string, 62 | ) => boolean; 63 | 64 | /** 65 | * The flag that controls whether the rule matches the "unsure" cases. For all 66 | * rules that extends this class, only one of them should set this to true, 67 | * otherwise we will get essentially duplicate finidngs. 68 | */ 69 | protected abstract readonly looseMatch: boolean; 70 | 71 | /** 72 | * Check if the attribute name is a literal in a setAttribute call. We will 73 | * skip matching if the attribute name is not in the blocklist. 74 | */ 75 | private isCalledWithAllowedAttribute( 76 | typeChecker: ts.TypeChecker, 77 | node: ts.CallExpression, 78 | ): boolean { 79 | // The 'setAttribute' function expects exactly two arguments: an attribute 80 | // name and a value. It's OK if someone provided the wrong number of 81 | // arguments because the code will have other compiler errors. 82 | if (node.arguments.length !== 2) return true; 83 | return this.isAllowedAttribute(typeChecker, node.arguments[0]); 84 | } 85 | 86 | /** 87 | * Check if the attribute name is a literal and the namespace is null in a 88 | * setAttributeNS call. We will skip matching if the attribute name is not in 89 | * the blocklist. 90 | */ 91 | private isCalledWithAllowedAttributeNS( 92 | typeChecker: ts.TypeChecker, 93 | node: ts.CallExpression, 94 | ): boolean { 95 | // The 'setAttributeNS' function expects exactly three arguments: a 96 | // namespace, an attribute name and a value. It's OK if someone provided the 97 | // wrong number of arguments because the code will have other compiler 98 | // errors. 99 | if (node.arguments.length !== 3) return true; 100 | return ( 101 | node.arguments[0].kind === ts.SyntaxKind.NullKeyword && 102 | this.isAllowedAttribute(typeChecker, node.arguments[1]) 103 | ); 104 | } 105 | 106 | /** 107 | * Check if the attribute name is a literal that is not in the blocklist. 108 | */ 109 | private isAllowedAttribute( 110 | typeChecker: ts.TypeChecker, 111 | attr: ts.Expression, 112 | ): boolean { 113 | const attrType = typeChecker.getTypeAtLocation(attr); 114 | if (this.looseMatch) { 115 | return ( 116 | attrType.isStringLiteral() && 117 | !this.isSecuritySensitiveAttrName(attrType.value.toLowerCase()) && 118 | isLiteral(typeChecker, attr) 119 | ); 120 | } else { 121 | return ( 122 | !attrType.isStringLiteral() || 123 | !isLiteral(typeChecker, attr) || 124 | !this.isSecuritySensitiveAttrName(attrType.value.toLowerCase()) 125 | ); 126 | } 127 | } 128 | 129 | private matchNode( 130 | tc: ts.TypeChecker, 131 | n: ts.PropertyAccessExpression | ts.ElementAccessExpression, 132 | matcher: PropertyMatcher, 133 | ) { 134 | if (!shouldExamineNode(n)) { 135 | return undefined; 136 | } 137 | 138 | if (!matcher.typeMatches(tc.getTypeAtLocation(n.expression))) { 139 | // Allowed: it is a different type. 140 | return undefined; 141 | } 142 | 143 | if (!ts.isCallExpression(n.parent)) { 144 | // Possibly not allowed: not calling it (may be renaming it). 145 | return this.looseMatch ? n : undefined; 146 | } 147 | 148 | if (n.parent.expression !== n) { 149 | // Possibly not allowed: calling a different function with it (may be 150 | // renaming it). 151 | return this.looseMatch ? n : undefined; 152 | } 153 | 154 | // If the matched node is a call to `setAttribute` (not setAttributeNS, etc) 155 | // and it's not setting a security sensitive attribute. 156 | if (matcher.bannedProperty === 'setAttribute') { 157 | const isAllowedAttr = this.isCalledWithAllowedAttribute(tc, n.parent); 158 | if (this.looseMatch) { 159 | // Allowed: it is not a security sensitive attribute. 160 | if (isAllowedAttr) return undefined; 161 | } else { 162 | return isAllowedAttr ? undefined : n; 163 | } 164 | } 165 | 166 | // If the matched node is a call to `setAttributeNS` with a null namespace 167 | // and it's not setting a security sensitive attribute. 168 | if (matcher.bannedProperty === 'setAttributeNS') { 169 | const isAllowedAttr = this.isCalledWithAllowedAttributeNS(tc, n.parent); 170 | if (this.looseMatch) { 171 | // Allowed: it is not a security sensitive attribute. 172 | if (isAllowedAttr) return undefined; 173 | } else { 174 | return isAllowedAttr ? undefined : n; 175 | } 176 | } 177 | 178 | return this.looseMatch ? n : undefined; 179 | } 180 | 181 | register(checker: Checker): void { 182 | for (const matcher of this.propMatchers) { 183 | checker.onNamedPropertyAccess( 184 | matcher.bannedProperty, 185 | (c, n) => { 186 | const node = this.matchNode(c.typeChecker, n, matcher); 187 | if (node) { 188 | checker.addFailureAtNode( 189 | node, 190 | this.errorMessage, 191 | this.ruleName, 192 | this.allowlist, 193 | ); 194 | } 195 | }, 196 | this.code, 197 | ); 198 | 199 | checker.onStringLiteralElementAccess( 200 | matcher.bannedProperty, 201 | (c, n) => { 202 | const node = this.matchNode(c.typeChecker, n, matcher); 203 | if (node) { 204 | checker.addFailureAtNode( 205 | node, 206 | this.errorMessage, 207 | this.ruleName, 208 | this.allowlist, 209 | ); 210 | } 211 | }, 212 | this.code, 213 | ); 214 | } 215 | } 216 | } 217 | 218 | let errMsg = 219 | 'Do not use Element#setAttribute or similar APIs, as this can lead to XSS or cause Trusted Types violations.'; 220 | 221 | /** A Rule that looks for use of Element#setAttribute and similar properties. */ 222 | export class Rule extends BanSetAttributeRule { 223 | static readonly RULE_NAME = 'ban-element-setattribute'; 224 | 225 | override readonly ruleName: string = Rule.RULE_NAME; 226 | 227 | protected readonly errorMessage: string = errMsg; 228 | protected isSecuritySensitiveAttrName = (attr: string): boolean => 229 | (attr.startsWith('on') && attr !== 'on') || TT_RELATED_ATTRIBUTES.has(attr); 230 | protected readonly looseMatch = true; 231 | 232 | constructor(configuration: RuleConfiguration = {}) { 233 | super(configuration); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_eval_calls.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; 21 | import {TRUSTED_SCRIPT} from '../../third_party/tsetse/util/trusted_types_configuration'; 22 | 23 | import {RuleConfiguration} from '../../rule_configuration'; 24 | 25 | let errMsg = 'Do not use eval, as this can lead to XSS.'; 26 | 27 | const bannedValues = [ 28 | 'GLOBAL|eval', 29 | ]; 30 | 31 | /** 32 | * A Rule that looks for references to the built-in eval() and window.eval() 33 | * methods. window.eval performs an indirect call to eval(), so a single check 34 | * for eval() bans both calls. 35 | */ 36 | export class Rule extends ConformancePatternRule { 37 | static readonly RULE_NAME = 'ban-eval-calls'; 38 | 39 | constructor(configuration: RuleConfiguration = {}) { 40 | super( 41 | overridePatternConfig({ 42 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 43 | errorMessage: errMsg, 44 | kind: PatternKind.BANNED_NAME, 45 | values: bannedValues, 46 | name: Rule.RULE_NAME, 47 | allowedTrustedType: TRUSTED_SCRIPT, 48 | ...configuration, 49 | }), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_function_calls.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {Allowlist} from '../../third_party/tsetse/allowlist'; 16 | import {Checker} from '../../third_party/tsetse/checker'; 17 | import {ErrorCode} from '../../third_party/tsetse/error_code'; 18 | import {AbstractRule} from '../../third_party/tsetse/rule'; 19 | import {AbsoluteMatcher} from '../../third_party/tsetse/util/absolute_matcher'; 20 | import {shouldExamineNode} from '../../third_party/tsetse/util/ast_tools'; 21 | import {isExpressionOfAllowedTrustedType} from '../../third_party/tsetse/util/is_trusted_type'; 22 | import {TRUSTED_SCRIPT} from '../../third_party/tsetse/util/trusted_types_configuration'; 23 | import * as ts from 'typescript'; 24 | 25 | import {RuleConfiguration} from '../../rule_configuration'; 26 | 27 | let errMsg = 'Constructing functions from strings can lead to XSS.'; 28 | 29 | /** 30 | * A Rule that looks for calls to the constructor of Function, either directly 31 | * or through Function.prototype.constructor. 32 | */ 33 | export class Rule extends AbstractRule { 34 | static readonly RULE_NAME = 'ban-function-calls'; 35 | readonly ruleName: string = Rule.RULE_NAME; 36 | readonly code: ErrorCode = ErrorCode.CONFORMANCE_PATTERN; 37 | 38 | private readonly allowTrustedTypes: boolean = true; 39 | private readonly nameMatcher: AbsoluteMatcher; 40 | private readonly allowlist?: Allowlist; 41 | 42 | constructor(configuration: RuleConfiguration = {}) { 43 | super(); 44 | this.nameMatcher = new AbsoluteMatcher('GLOBAL|Function'); 45 | if (configuration?.allowlistEntries) { 46 | this.allowlist = new Allowlist(configuration?.allowlistEntries); 47 | } 48 | } 49 | 50 | register(checker: Checker): void { 51 | const check = (c: Checker, n: ts.Node) => { 52 | const node = this.checkNode(c.typeChecker, n, this.nameMatcher); 53 | if (node) { 54 | checker.addFailureAtNode(node, errMsg, Rule.RULE_NAME, this.allowlist); 55 | } 56 | }; 57 | checker.onNamedIdentifier(this.nameMatcher.bannedName, check, this.code); 58 | checker.onStringLiteralElementAccess( 59 | 'Function', 60 | (c, n) => { 61 | check(c, n.argumentExpression); 62 | }, 63 | this.code, 64 | ); 65 | } 66 | 67 | private checkNode( 68 | tc: ts.TypeChecker, 69 | n: ts.Node, 70 | matcher: AbsoluteMatcher, 71 | ): ts.Node | undefined { 72 | let matched: (ts.Node & {arguments: readonly ts.Expression[]}) | undefined = 73 | undefined; 74 | 75 | if (!shouldExamineNode(n)) return; 76 | if (!matcher.matches(n, tc)) return; 77 | 78 | if (!n.parent) return; 79 | 80 | // Function can be accessed through window or other globalThis objects 81 | // through the dot or bracket syntax. Check if we are seeing one of these 82 | // cases 83 | if ( 84 | (ts.isPropertyAccessExpression(n.parent) && n.parent.name === n) || 85 | ts.isElementAccessExpression(n.parent) 86 | ) { 87 | n = n.parent; 88 | } 89 | // Additionally cover the case `(Function)('bad script')`. 90 | // Note: there can be parentheses in every expression but we cann't afford 91 | // to check all of them. Leave other cases unchecked until we see real 92 | // bypasses. 93 | if (ts.isParenthesizedExpression(n.parent)) { 94 | n = n.parent; 95 | } 96 | 97 | const parent = n.parent; 98 | 99 | // Check if the matched node is part of a `new Function(string)` or 100 | // `Function(string)` expression 101 | if (ts.isNewExpression(parent) || ts.isCallExpression(parent)) { 102 | if (parent.expression === n && parent.arguments?.length) { 103 | matched = parent as Exclude; 104 | } 105 | } else { 106 | if (!parent.parent || !parent.parent.parent) return; 107 | 108 | // Check if the matched node is part of a 109 | // `Function.prototype.constructor(string)` expression. 110 | if ( 111 | ts.isPropertyAccessExpression(parent) && 112 | parent.name.text === 'prototype' && 113 | ts.isPropertyAccessExpression(parent.parent) && 114 | parent.parent.name.text === 'constructor' && 115 | ts.isCallExpression(parent.parent.parent) && 116 | parent.parent.parent.expression === parent.parent && 117 | parent.parent.parent.arguments.length 118 | ) { 119 | matched = parent.parent.parent; 120 | } 121 | } 122 | 123 | // If the constructor is called with TrustedScript arguments, do not flag it 124 | // (if the rule is confugired this way). 125 | if ( 126 | matched && 127 | this.allowTrustedTypes && 128 | matched.arguments.every((arg) => 129 | isExpressionOfAllowedTrustedType(tc, arg, TRUSTED_SCRIPT), 130 | ) 131 | ) { 132 | return; 133 | } 134 | 135 | return matched; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_iframe_srcdoc_assignments.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {Fix} from '../../third_party/tsetse/failure'; 16 | import { 17 | ConformancePatternRule, 18 | ErrorCode, 19 | PatternKind, 20 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 21 | import {maybeAddNamedImport} from '../../third_party/tsetse/util/fixer'; 22 | import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; 23 | import {TRUSTED_HTML} from '../../third_party/tsetse/util/trusted_types_configuration'; 24 | import * as ts from 'typescript'; 25 | 26 | import {RuleConfiguration} from '../../rule_configuration'; 27 | 28 | let errMsg = 29 | 'Assigning directly to HTMLIFrameElement#srcdoc can result in XSS vulnerabilities.'; 30 | 31 | /** 32 | * A Rule that looks for assignments to an HTMLIFrameElement's srcdoc property. 33 | */ 34 | export class Rule extends ConformancePatternRule { 35 | static readonly RULE_NAME = 'ban-iframe-srcdoc-assignments'; 36 | constructor(configuration: RuleConfiguration = {}) { 37 | super( 38 | overridePatternConfig({ 39 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 40 | errorMessage: errMsg, 41 | kind: PatternKind.BANNED_PROPERTY_WRITE, 42 | values: ['HTMLIFrameElement.prototype.srcdoc'], 43 | name: Rule.RULE_NAME, 44 | allowedTrustedType: TRUSTED_HTML, 45 | ...configuration, 46 | }), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_object_data_assignments.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; 21 | import {TRUSTED_SCRIPT_URL} from '../../third_party/tsetse/util/trusted_types_configuration'; 22 | 23 | import {RuleConfiguration} from '../../rule_configuration'; 24 | 25 | let errMsg = 26 | 'Do not assign variables to HTMLObjectElement#data, as this can lead to XSS.'; 27 | 28 | /** 29 | * A Rule that looks for dynamic assignments to an HTMLScriptElement's src 30 | * property, and suggests using safe setters instead. 31 | */ 32 | export class Rule extends ConformancePatternRule { 33 | static readonly RULE_NAME = 'ban-object-data-assignments'; 34 | 35 | constructor(configuration: RuleConfiguration = {}) { 36 | super( 37 | overridePatternConfig({ 38 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 39 | errorMessage: errMsg, 40 | kind: PatternKind.BANNED_PROPERTY_WRITE, 41 | values: ['HTMLObjectElement.prototype.data'], 42 | ...configuration, 43 | name: Rule.RULE_NAME, 44 | allowedTrustedType: TRUSTED_SCRIPT_URL, 45 | }), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_range_createcontextualfragment.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; 21 | import {TRUSTED_HTML} from '../../third_party/tsetse/util/trusted_types_configuration'; 22 | 23 | import {RuleConfiguration} from '../../rule_configuration'; 24 | 25 | let errMsg = 26 | 'Do not use Range#createContextualFragment, as this can lead to XSS.'; 27 | 28 | /** 29 | * A Rule that looks for use of Range#createContextualFragment properties. 30 | */ 31 | export class Rule extends ConformancePatternRule { 32 | static readonly RULE_NAME = 'ban-range-createcontextualfragment'; 33 | 34 | constructor(configuration: RuleConfiguration = {}) { 35 | super( 36 | overridePatternConfig({ 37 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 38 | errorMessage: errMsg, 39 | kind: PatternKind.BANNED_PROPERTY, 40 | values: ['Range.prototype.createContextualFragment'], 41 | name: Rule.RULE_NAME, 42 | allowedTrustedType: TRUSTED_HTML, 43 | ...configuration, 44 | }), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_script_appendchild_calls.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | import {RuleConfiguration} from '../../rule_configuration'; 21 | 22 | let errMsg = 23 | 'Do not use HTMLScriptElement#appendChild because it is similar to eval and can cause code-injection security vulnerabilities.'; 24 | 25 | /** A rule that bans the use of HTMLScriptElement#appendChild */ 26 | export class Rule extends ConformancePatternRule { 27 | static readonly RULE_NAME = 'ban-script-appendchild-calls'; 28 | 29 | constructor(configuration: RuleConfiguration = {}) { 30 | super({ 31 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 32 | errorMessage: errMsg, 33 | kind: PatternKind.BANNED_PROPERTY, 34 | values: ['HTMLScriptElement.prototype.appendChild'], 35 | name: Rule.RULE_NAME, 36 | ...configuration, 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_script_content_assignments.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; 21 | import {TRUSTED_SCRIPT} from '../../third_party/tsetse/util/trusted_types_configuration'; 22 | 23 | import {RuleConfiguration} from '../../rule_configuration'; 24 | 25 | let errMsg = 26 | 'Do not assign values to HTMLScriptElement#text or HTMLScriptElement#textContent, as this can lead to XSS.'; 27 | 28 | /** 29 | * A rule that bans writing to HTMLScriptElement#text and 30 | * HTMLScriptElement#textContent 31 | */ 32 | export class Rule extends ConformancePatternRule { 33 | static readonly RULE_NAME = 'ban-script-content-assignments'; 34 | 35 | constructor(configuration: RuleConfiguration = {}) { 36 | super( 37 | overridePatternConfig({ 38 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 39 | errorMessage: errMsg, 40 | kind: PatternKind.BANNED_PROPERTY_WRITE, 41 | values: [ 42 | 'HTMLScriptElement.prototype.innerText', 43 | 'HTMLScriptElement.prototype.text', 44 | 'HTMLScriptElement.prototype.textContent', 45 | ], 46 | name: Rule.RULE_NAME, 47 | allowedTrustedType: TRUSTED_SCRIPT, 48 | ...configuration, 49 | }), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_script_src_assignments.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; 21 | import {TRUSTED_SCRIPT_URL} from '../../third_party/tsetse/util/trusted_types_configuration'; 22 | 23 | import {RuleConfiguration} from '../../rule_configuration'; 24 | 25 | let errMsg = 26 | 'Do not assign variables to HTMLScriptElement#src, as this can lead to XSS.'; 27 | 28 | /** 29 | * A Rule that looks for dynamic assignments to an HTMLScriptElement's src 30 | * property, and suggests using safe setters instead. 31 | */ 32 | export class Rule extends ConformancePatternRule { 33 | static readonly RULE_NAME = 'ban-script-src-assignments'; 34 | 35 | constructor(configuration: RuleConfiguration = {}) { 36 | super( 37 | overridePatternConfig({ 38 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 39 | errorMessage: errMsg, 40 | kind: PatternKind.BANNED_PROPERTY_WRITE, 41 | values: ['HTMLScriptElement.prototype.src'], 42 | ...configuration, 43 | name: Rule.RULE_NAME, 44 | allowedTrustedType: TRUSTED_SCRIPT_URL, 45 | }), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_serviceworkercontainer_register.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; 21 | import {TRUSTED_SCRIPT_URL} from '../../third_party/tsetse/util/trusted_types_configuration'; 22 | 23 | import {RuleConfiguration} from '../../rule_configuration'; 24 | 25 | let errMsg = 26 | 'Do not use ServiceWorkerContainer#register, as this can lead to XSS.'; 27 | 28 | /** 29 | * A Rule that looks for use of Element#insertAdjacentHTML properties. 30 | */ 31 | export class Rule extends ConformancePatternRule { 32 | static readonly RULE_NAME = 'ban-serviceworkercontainer-register'; 33 | 34 | constructor(configuration: RuleConfiguration = {}) { 35 | super( 36 | overridePatternConfig({ 37 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 38 | errorMessage: errMsg, 39 | kind: PatternKind.BANNED_PROPERTY, 40 | values: ['ServiceWorkerContainer.prototype.register'], 41 | name: Rule.RULE_NAME, 42 | allowedTrustedType: TRUSTED_SCRIPT_URL, 43 | ...configuration, 44 | }), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_shared_worker_calls.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; 21 | import {TRUSTED_SCRIPT_URL} from '../../third_party/tsetse/util/trusted_types_configuration'; 22 | 23 | import {RuleConfiguration} from '../../rule_configuration'; 24 | 25 | let errMsg = 26 | 'Constructing shared Web Workers can cause code to be loaded from an untrusted URL.'; 27 | 28 | /** 29 | * A Rule that looks for calls to create new Workers and suggests using a safe 30 | * creator instead. 31 | */ 32 | export class Rule extends ConformancePatternRule { 33 | static readonly RULE_NAME = 'ban-shared-worker-calls'; 34 | 35 | constructor(configuration: RuleConfiguration = {}) { 36 | super( 37 | overridePatternConfig({ 38 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 39 | errorMessage: errMsg, 40 | kind: PatternKind.BANNED_NAME, 41 | values: ['GLOBAL|SharedWorker'], 42 | name: Rule.RULE_NAME, 43 | allowedTrustedType: TRUSTED_SCRIPT_URL, 44 | ...configuration, 45 | }), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_trustedtypes_createpolicy.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | import {RuleConfiguration} from '../../rule_configuration'; 21 | 22 | let errMsg = 'Creating a Trusted Types policy requires a security review.'; 23 | 24 | /** 25 | * A rule that bans TrustedTypeProlicyFactory#createPolicy. 26 | */ 27 | export class Rule extends ConformancePatternRule { 28 | static readonly RULE_NAME = 'ban-trustedtypes-createpolicy'; 29 | 30 | constructor(configuration: RuleConfiguration = {}) { 31 | super({ 32 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 33 | errorMessage: errMsg, 34 | kind: PatternKind.BANNED_PROPERTY, 35 | values: ['TrustedTypePolicyFactory.prototype.createPolicy'], 36 | name: Rule.RULE_NAME, 37 | ...configuration, 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_window_stringfunctiondef.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * @fileoverview Turn on a TS security checker to ban setInverval and setTimeout 17 | * when they are called to evaluate strings as scripts. 18 | * 19 | * Unlike other rules that only look at the property/name, this rule checks if 20 | * the functions are called with strings as the first argument. Therefore, none 21 | * of the pattern engines directly applies to this rule. We could have used 22 | * BANNED_NAME and BANNED_PROPERTY like we did for open and eval, but it causes 23 | * too many false positives in this case. 24 | */ 25 | 26 | import {Allowlist} from '../../third_party/tsetse/allowlist'; 27 | import {Checker} from '../../third_party/tsetse/checker'; 28 | import {ErrorCode} from '../../third_party/tsetse/error_code'; 29 | import {AbstractRule} from '../../third_party/tsetse/rule'; 30 | import {AbsoluteMatcher} from '../../third_party/tsetse/util/absolute_matcher'; 31 | import {shouldExamineNode} from '../../third_party/tsetse/util/ast_tools'; 32 | import {isExpressionOfAllowedTrustedType} from '../../third_party/tsetse/util/is_trusted_type'; 33 | import {PropertyMatcher} from '../../third_party/tsetse/util/property_matcher'; 34 | import {TRUSTED_SCRIPT} from '../../third_party/tsetse/util/trusted_types_configuration'; 35 | import * as ts from 'typescript'; 36 | 37 | import {RuleConfiguration} from '../../rule_configuration'; 38 | 39 | const BANNED_NAMES = ['GLOBAL|setInterval', 'GLOBAL|setTimeout']; 40 | 41 | const BANNED_PROPERTIES = [ 42 | 'Window.prototype.setInterval', 43 | 'Window.prototype.setTimeout', 44 | ]; 45 | 46 | function formatErrorMessage(bannedEntity: string): string { 47 | let errMsg = `Do not use ${bannedEntity}, as calling it with a string argument can cause code-injection security vulnerabilities.`; 48 | return errMsg; 49 | } 50 | 51 | /** 52 | * Checks if the APIs are called with functions (that 53 | * won't trigger an eval-like effect) or a TrustedScript value if Trusted Types 54 | * are enabled (and it's up to the developer to make sure it 55 | * the value can't be misused). These patterns are safe to use, so we want to 56 | * exclude them from the reported errors. 57 | */ 58 | function isUsedWithNonStringArgument(n: ts.Node, tc: ts.TypeChecker) { 59 | const par = n.parent; 60 | // Early return on pattern like `const st = setTimeout;` We now consider this 61 | // pattern acceptable to reduce false positives. 62 | if (!ts.isCallExpression(par) || par.expression !== n) return true; 63 | // Having zero arguments will trigger other compiler errors. We should not 64 | // bother emitting a Tsetse error. 65 | if (par.arguments.length === 0) return true; 66 | 67 | const firstArgType = tc.getTypeAtLocation(par.arguments[0]); 68 | 69 | const isFirstArgNonString = 70 | (firstArgType.flags & 71 | (ts.TypeFlags.String | 72 | ts.TypeFlags.StringLike | 73 | ts.TypeFlags.StringLiteral)) === 74 | 0; 75 | if (isExpressionOfAllowedTrustedType(tc, par.arguments[0], TRUSTED_SCRIPT)) { 76 | return true; 77 | } 78 | return isFirstArgNonString; 79 | } 80 | 81 | function isBannedStringLiteralAccess( 82 | n: ts.ElementAccessExpression, 83 | tc: ts.TypeChecker, 84 | propMatcher: PropertyMatcher, 85 | ) { 86 | const argExp = n.argumentExpression; 87 | return ( 88 | propMatcher.typeMatches(tc.getTypeAtLocation(n.expression)) && 89 | ts.isStringLiteralLike(argExp) && 90 | argExp.text === propMatcher.bannedProperty 91 | ); 92 | } 93 | 94 | /** 95 | * A type selector that resolves to AbsoluteMatcher or PropertyMatcher based on 96 | * the type of AST node to be matched. 97 | */ 98 | type NodeMatcher = T extends ts.Identifier 99 | ? AbsoluteMatcher 100 | : T extends ts.PropertyAccessExpression 101 | ? PropertyMatcher 102 | : T extends ts.ElementAccessExpression 103 | ? { 104 | matches: ( 105 | n: ts.ElementAccessExpression, 106 | tc: ts.TypeChecker, 107 | ) => boolean; 108 | } 109 | : {matches: (n: ts.Node, tc: ts.TypeChecker) => never}; 110 | 111 | function checkNode( 112 | tc: ts.TypeChecker, 113 | n: T, 114 | matcher: NodeMatcher, 115 | ): ts.Node | undefined { 116 | if (!shouldExamineNode(n)) return; 117 | // TODO: go/ts54upgrade - Auto-added to unblock TS5.4 migration. 118 | // TS2345: Argument of type 'T' is not assignable to parameter of type 'never'. 119 | // @ts-ignore 120 | if (!matcher.matches(n, tc)) return; 121 | if (isUsedWithNonStringArgument(n, tc)) return; 122 | return n; 123 | } 124 | 125 | /** 126 | * A rule that checks the uses of Window#setTimeout and Window#setInterval 127 | * properties; it also checks the global setTimeout and setInterval functions. 128 | */ 129 | export class Rule extends AbstractRule { 130 | static readonly RULE_NAME = 'ban-window-stringfunctiondef'; 131 | readonly ruleName: string = Rule.RULE_NAME; 132 | readonly code: ErrorCode = ErrorCode.CONFORMANCE_PATTERN; 133 | 134 | private readonly nameMatchers: readonly AbsoluteMatcher[]; 135 | private readonly propMatchers: readonly PropertyMatcher[]; 136 | 137 | private readonly allowlist?: Allowlist; 138 | 139 | constructor(configuration: RuleConfiguration = {}) { 140 | super(); 141 | this.nameMatchers = BANNED_NAMES.map((name) => new AbsoluteMatcher(name)); 142 | this.propMatchers = BANNED_PROPERTIES.map(PropertyMatcher.fromSpec); 143 | if (configuration?.allowlistEntries) { 144 | this.allowlist = new Allowlist(configuration?.allowlistEntries); 145 | } 146 | } 147 | 148 | register(checker: Checker): void { 149 | // Check global names 150 | for (const nameMatcher of this.nameMatchers) { 151 | checker.onNamedIdentifier( 152 | nameMatcher.bannedName, 153 | (c, n) => { 154 | // window.id is automatically resolved to id, so the matcher will be 155 | // able to match it. But we don't want redundant errors. Skip the 156 | // node if it is part of a property access expression. 157 | if (ts.isPropertyAccessExpression(n.parent)) return; 158 | if (ts.isQualifiedName(n.parent)) return; 159 | 160 | const node = checkNode(c.typeChecker, n, nameMatcher); 161 | if (node) { 162 | checker.addFailureAtNode( 163 | node, 164 | formatErrorMessage(nameMatcher.bannedName), 165 | Rule.RULE_NAME, 166 | this.allowlist, 167 | ); 168 | } 169 | }, 170 | this.code, 171 | ); 172 | } 173 | // Check properties 174 | for (const propMatcher of this.propMatchers) { 175 | checker.onNamedPropertyAccess( 176 | propMatcher.bannedProperty, 177 | (c, n) => { 178 | const node = checkNode(c.typeChecker, n, propMatcher); 179 | if (node) { 180 | checker.addFailureAtNode( 181 | node, 182 | formatErrorMessage( 183 | `${propMatcher.bannedType}#${propMatcher.bannedProperty}`, 184 | ), 185 | Rule.RULE_NAME, 186 | this.allowlist, 187 | ); 188 | } 189 | }, 190 | this.code, 191 | ); 192 | 193 | checker.onStringLiteralElementAccess( 194 | propMatcher.bannedProperty, 195 | (c, n) => { 196 | const node = checkNode(c.typeChecker, n, { 197 | matches: () => 198 | isBannedStringLiteralAccess(n, c.typeChecker, propMatcher), 199 | }); 200 | if (node) { 201 | checker.addFailureAtNode( 202 | node, 203 | formatErrorMessage( 204 | `${propMatcher.bannedType}#${propMatcher.bannedProperty}`, 205 | ), 206 | Rule.RULE_NAME, 207 | this.allowlist, 208 | ); 209 | } 210 | }, 211 | this.code, 212 | ); 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_worker_calls.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; 21 | import {TRUSTED_SCRIPT_URL} from '../../third_party/tsetse/util/trusted_types_configuration'; 22 | 23 | import {RuleConfiguration} from '../../rule_configuration'; 24 | 25 | let errMsg = 26 | 'Constructing Web Workers can cause code to be loaded from an untrusted URL.'; 27 | 28 | /** 29 | * A Rule that looks for calls to create new Workers and suggests using a safe 30 | * creator instead. 31 | */ 32 | export class Rule extends ConformancePatternRule { 33 | static readonly RULE_NAME = 'ban-worker-calls'; 34 | 35 | constructor(configuration: RuleConfiguration = {}) { 36 | super( 37 | overridePatternConfig({ 38 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 39 | errorMessage: errMsg, 40 | kind: PatternKind.BANNED_NAME, 41 | values: ['GLOBAL|Worker'], 42 | name: Rule.RULE_NAME, 43 | allowedTrustedType: TRUSTED_SCRIPT_URL, 44 | ...configuration, 45 | }), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /common/rules/dom_security/ban_worker_importscripts.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; 21 | import {TRUSTED_SCRIPT_URL} from '../../third_party/tsetse/util/trusted_types_configuration'; 22 | 23 | import {RuleConfiguration} from '../../rule_configuration'; 24 | 25 | let errMsg = 26 | 'Do not call importScripts in web workers, as this can lead to XSS.'; 27 | 28 | /** A Rule that bans the importScripts function in worker global scopes. */ 29 | export class Rule extends ConformancePatternRule { 30 | static readonly RULE_NAME = 'ban-worker-importscripts'; 31 | 32 | constructor(configuration: RuleConfiguration = {}) { 33 | super( 34 | overridePatternConfig({ 35 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 36 | errorMessage: errMsg, 37 | kind: PatternKind.BANNED_NAME, 38 | values: ['GLOBAL|importScripts'], 39 | name: Rule.RULE_NAME, 40 | allowedTrustedType: TRUSTED_SCRIPT_URL, 41 | ...configuration, 42 | }), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /common/rules/unsafe/ban_legacy_conversions.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | 21 | import {RuleConfiguration} from '../../rule_configuration'; 22 | 23 | let errMsg = 24 | 'Use of legacy conversions to safe values requires security reviews and approval.'; 25 | 26 | let bannedValues = [ 27 | '/node_modules/safevalues/restricted/legacy|legacyUnsafeHtml', 28 | '/node_modules/safevalues/restricted/legacy|legacyUnsafeScript', 29 | '/node_modules/safevalues/restricted/legacy|legacyUnsafeScriptUrl', 30 | // Deprecated API, keep banning for now in case people are using an older 31 | // version of safevalues 32 | '/node_modules/safevalues/restricted/legacy|legacyConversionToHtml', 33 | '/node_modules/safevalues/restricted/legacy|legacyConversionToScript', 34 | '/node_modules/safevalues/restricted/legacy|legacyConversionToScriptUrl', 35 | '/node_modules/safevalues/unsafe/legacy|legacyConversionToHtml', 36 | '/node_modules/safevalues/unsafe/legacy|legacyConversionToScript', 37 | '/node_modules/safevalues/unsafe/legacy|legacyConversionToScriptUrl', 38 | ]; 39 | 40 | /** A Rule that bans the use of legacy conversions to safe values. */ 41 | export class Rule extends ConformancePatternRule { 42 | static readonly RULE_NAME = 'ban-legacy-conversions'; 43 | 44 | constructor(configuration: RuleConfiguration = {}) { 45 | super({ 46 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 47 | errorMessage: errMsg, 48 | values: bannedValues, 49 | kind: PatternKind.BANNED_NAME, 50 | name: Rule.RULE_NAME, 51 | ...configuration, 52 | }); 53 | 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /common/rules/unsafe/ban_reviewed_conversions.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | ConformancePatternRule, 17 | ErrorCode, 18 | PatternKind, 19 | } from '../../third_party/tsetse/rules/conformance_pattern_rule'; 20 | 21 | import {RuleConfiguration} from '../../rule_configuration'; 22 | 23 | let errMsg = 24 | 'Use of reviewed conversions to safe values requires security reviews and approval.'; 25 | 26 | let bannedValues = [ 27 | '/node_modules/safevalues/restricted/reviewed|htmlSafeByReview', 28 | '/node_modules/safevalues/restricted/reviewed|scriptSafeByReview', 29 | '/node_modules/safevalues/restricted/reviewed|scriptUrlSafeByReview', 30 | // Deprecated API, keep banning for now in case people are using an older 31 | // version of safevalues 32 | '/node_modules/safevalues/restricted/reviewed|htmlFromStringKnownToSatisfyTypeContract', 33 | '/node_modules/safevalues/restricted/reviewed|scriptFromStringKnownToSatisfyTypeContract', 34 | '/node_modules/safevalues/restricted/reviewed|scriptUrlFromStringKnownToSatisfyTypeContract', 35 | '/node_modules/safevalues/unsafe/reviewed|htmlFromStringKnownToSatisfyTypeContract', 36 | '/node_modules/safevalues/unsafe/reviewed|scriptFromStringKnownToSatisfyTypeContract', 37 | '/node_modules/safevalues/unsafe/reviewed|scriptUrlFromStringKnownToSatisfyTypeContract', 38 | ]; 39 | 40 | /** A Rule that bans the use of reviewed conversions to safe values. */ 41 | export class Rule extends ConformancePatternRule { 42 | static readonly RULE_NAME = 'ban-reviewed-conversions'; 43 | 44 | constructor(configuration: RuleConfiguration = {}) { 45 | super({ 46 | errorCode: ErrorCode.CONFORMANCE_PATTERN, 47 | errorMessage: errMsg, 48 | kind: PatternKind.BANNED_NAME, 49 | values: bannedValues, 50 | name: Rule.RULE_NAME, 51 | ...configuration, 52 | }); 53 | 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /common/third_party/tsetse/allowlist.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An exemption list entry, corresponding to a logical exemption rule. Use these 3 | * to distinguish between various logical reasons for exempting something: 4 | * for instance, tie these to particular bugs that needed to be exempted, per 5 | * legacy project, manually reviewed entries, and so on. 6 | * 7 | * Exemption lists are based on the file paths provided by the TS compiler, with 8 | * both regexp-based checks and prefix-based checks. 9 | * 10 | * 11 | * Follows the logic in 12 | * https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/conformance.proto. 13 | */ 14 | export interface AllowlistEntry { 15 | /** The category corresponding to this entry. */ 16 | readonly reason: ExemptionReason; 17 | /** Why is this okay to be exempted?. */ 18 | readonly explanation?: string; 19 | 20 | /** 21 | * Regexps for the paths of files that will be ignored by the 22 | * ConformancePattern. Beware, escaping can be tricky. 23 | */ 24 | readonly regexp?: readonly string[]; 25 | /** Exact path of the file that will be ignored by the ConformancePattern. */ 26 | readonly path?: readonly string[]; 27 | } 28 | 29 | /** 30 | * The categories of exemption entries. 31 | */ 32 | export enum ExemptionReason { 33 | /** No reason. */ 34 | UNSPECIFIED, 35 | /** Code that has to be grandfathered in (no guarantees). */ 36 | LEGACY, 37 | /** 38 | * Code that does not enter the scope of this particular check (no 39 | * guarantees). 40 | */ 41 | OUT_OF_SCOPE, 42 | /** Manually reviewed exceptions (supposedly okay). */ 43 | MANUALLY_REVIEWED, 44 | } 45 | 46 | /** 47 | * A complete allowlist with all related AllowlistEntry grouped together, with 48 | * ExemptionReason ignored since it is purely for documentary purposes. 49 | */ 50 | export class Allowlist { 51 | private readonly allowlistedPaths: readonly string[] = []; 52 | private readonly allowlistedRegExps: readonly RegExp[] = []; 53 | // To avoid repeated computation for allowlisting queries with the same file 54 | // path, create a memoizer to cache known results. This is useful in watch 55 | // mode (and possible in language service) when the same files can be compiled 56 | // repeatedly. 57 | private readonly allowlistMemoizer = new Map(); 58 | 59 | constructor( 60 | allowlistEntries?: AllowlistEntry[], 61 | removePrefixes: string[] = [], 62 | ) { 63 | if (allowlistEntries) { 64 | for (const e of allowlistEntries) { 65 | if (e.path) { 66 | this.allowlistedPaths = this.allowlistedPaths.concat(...e.path); 67 | } 68 | if (e.regexp) { 69 | this.allowlistedRegExps = this.allowlistedRegExps.concat( 70 | ...e.regexp.map((r) => new RegExp(r)), 71 | ); 72 | } 73 | } 74 | } 75 | } 76 | 77 | isAllowlisted(filePath: string): boolean { 78 | if (this.allowlistMemoizer.has(filePath)) { 79 | return this.allowlistMemoizer.get(filePath)!; 80 | } 81 | for (const p of this.allowlistedPaths) { 82 | if (filePath === p) { 83 | this.allowlistMemoizer.set(filePath, true); 84 | return true; 85 | } 86 | if (p.endsWith('/') && filePath.startsWith(p)) { 87 | this.allowlistMemoizer.set(filePath, true); 88 | return true; 89 | } 90 | } 91 | for (const re of this.allowlistedRegExps) { 92 | if (re.test(filePath)) { 93 | this.allowlistMemoizer.set(filePath, true); 94 | return true; 95 | } 96 | } 97 | this.allowlistMemoizer.set(filePath, false); 98 | return false; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /common/third_party/tsetse/error_code.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Error codes for tsetse checks. 3 | * 4 | * Start with 21222 and increase linearly. 5 | * The intent is for these codes to be fixed, so that tsetse users can 6 | * search for them in user forums and other media. 7 | */ 8 | export enum ErrorCode { 9 | CHECK_RETURN_VALUE = 21222, 10 | EQUALS_NAN = 21223, 11 | BAN_EXPECT_TRUTHY_PROMISE = 21224, 12 | MUST_USE_PROMISES = 21225, 13 | BAN_PROMISE_AS_CONDITION = 21226, 14 | PROPERTY_RENAMING_SAFE = 21227, 15 | CONFORMANCE_PATTERN = 21228, 16 | BAN_MUTABLE_EXPORTS = 21229, 17 | BAN_STRING_INITIALIZED_SETS = 21230, 18 | BAD_SIDE_EFFECT_IMPORT = 21232, 19 | BAN_AT_INTERNAL = 21233, 20 | ISOLATED_DECORATOR_METADATA = 21234, 21 | } 22 | -------------------------------------------------------------------------------- /common/third_party/tsetse/failure.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | /** 4 | * A Tsetse check Failure is almost identical to a Diagnostic from TypeScript 5 | * except that: 6 | * (1) The error code is defined by each individual Tsetse rule. 7 | * (2) The optional `source` property is set to `Tsetse` so the host (VS Code 8 | * for instance) would use that to indicate where the error comes from. 9 | * (3) There's an optional suggestedFixes field. 10 | */ 11 | export class Failure { 12 | constructor( 13 | private readonly sourceFile: ts.SourceFile, 14 | private readonly start: number, 15 | private readonly end: number, 16 | private readonly failureText: string, 17 | private readonly code: number, 18 | /** 19 | * The origin of the failure, e.g., the name of the rule reporting the 20 | * failure. Can be empty. 21 | */ 22 | private readonly failureSource: string | undefined, 23 | private readonly suggestedFixes: Fix[] = [], 24 | private readonly relatedInformation?: ts.DiagnosticRelatedInformation[], 25 | ) {} 26 | 27 | /** 28 | * This returns a structure compatible with ts.Diagnostic, but with added 29 | * fields, for convenience and to support suggested fixes. 30 | */ 31 | toDiagnostic(): DiagnosticWithFixes { 32 | return { 33 | file: this.sourceFile, 34 | start: this.start, 35 | end: this.end, // Not in ts.Diagnostic, but always useful for 36 | // start-end-using systems. 37 | length: this.end - this.start, 38 | // Emebed `failureSource` into the error message so that we can show 39 | // people which check they are violating. This makes it easier for 40 | // developers to configure exemptions. 41 | messageText: this.failureSource 42 | ? `[${this.failureSource}] ${this.failureText}` 43 | : this.failureText, 44 | category: ts.DiagnosticCategory.Error, 45 | code: this.code, 46 | // Other tools like TSLint can use this field to decide the subcategory of 47 | // the diagnostic. 48 | source: this.failureSource, 49 | fixes: this.suggestedFixes, 50 | relatedInformation: this.relatedInformation, 51 | }; 52 | } 53 | 54 | /** 55 | * Same as toDiagnostic, but include the fix in the message, so that systems 56 | * that don't support displaying suggested fixes can still surface that 57 | * information. This assumes the diagnostic message is going to be presented 58 | * within the context of the problematic code. 59 | */ 60 | toDiagnosticWithStringifiedFixes(): DiagnosticWithFixes { 61 | const diagnostic = this.toDiagnostic(); 62 | if (this.suggestedFixes.length) { 63 | // Add a space between the violation text and fix message. 64 | diagnostic.messageText += ' ' + this.mapFixesToReadableString(); 65 | } 66 | return diagnostic; 67 | } 68 | 69 | toString(): string { 70 | return `{ sourceFile:${ 71 | this.sourceFile ? this.sourceFile.fileName : 'unknown' 72 | }, start:${ 73 | this.start 74 | }, end:${this.end}, source:${this.failureSource}, fixes:${JSON.stringify( 75 | this.suggestedFixes.map((fix) => fixToString(fix)), 76 | )} }`; 77 | } 78 | 79 | /*** 80 | * Stringifies an array of `suggestedFixes` for this failure. This is just a 81 | * heuristic and should be used in systems which do not support fixers 82 | * integration (e.g. CLI tools). 83 | */ 84 | mapFixesToReadableString(): string { 85 | const stringifiedFixes = this.suggestedFixes 86 | .map((fix) => this.fixToReadableString(fix)) 87 | .join('\nOR\n'); 88 | 89 | if (!stringifiedFixes) return ''; 90 | else if (this.suggestedFixes.length === 1) { 91 | return 'Suggested fix:\n' + stringifiedFixes; 92 | } else { 93 | return 'Suggested fixes:\n' + stringifiedFixes; 94 | } 95 | } 96 | 97 | /** 98 | * Stringifies a `Fix`, in a way that makes sense when presented alongside the 99 | * finding. This is a heuristic, obviously. 100 | */ 101 | fixToReadableString(f: Fix) { 102 | let fixText = ''; 103 | 104 | for (const c of f.changes) { 105 | // Remove leading/trailing whitespace from the stringified suggestions: 106 | // since we add line breaks after each line of stringified suggestion, and 107 | // since users will manually apply the fix, there is no need to show 108 | // trailing whitespace. This is however just for stringification of the 109 | // fixes: the suggested fix itself still keeps trailing whitespace. 110 | const printableReplacement = c.replacement.trim(); 111 | 112 | // Insertion. 113 | if (c.start === c.end) { 114 | // Try to see if that's an import. 115 | if (c.replacement.indexOf('import') !== -1) { 116 | fixText += `- Add new import: ${printableReplacement}\n`; 117 | } else { 118 | // Insertion that's not a full import. This should rarely happen in 119 | // our context, and we don't have a great message for these. 120 | // For instance, this could be the addition of a new symbol in an 121 | // existing import (`import {foo}` becoming `import {foo, bar}`). 122 | fixText += `- Insert ${this.readableRange(c.start, c.end)}: ${printableReplacement}\n`; 123 | } 124 | } else if (c.start === this.start && c.end === this.end) { 125 | // We assume the replacement is the main part of the fix, so put that 126 | // individual change first in `fixText`. 127 | if (printableReplacement === '') { 128 | fixText = `- Delete the full match\n` + fixText; 129 | } else { 130 | fixText = 131 | `- Replace the full match with: ${printableReplacement}\n` + 132 | fixText; 133 | } 134 | } else { 135 | if (printableReplacement === '') { 136 | fixText = 137 | `- Delete ${this.readableRange(c.start, c.end)}\n` + fixText; 138 | } else { 139 | fixText = 140 | `- Replace ${this.readableRange(c.start, c.end)} with: ` + 141 | `${printableReplacement}\n${fixText}`; 142 | } 143 | } 144 | } 145 | 146 | return fixText.trim(); 147 | } 148 | 149 | /** 150 | * Turns the range to a human readable format to be used by fixers. 151 | * 152 | * If the length of the range is non zero it returns the source file text 153 | * representing the range. Otherwise returns the stringified representation of 154 | * the source file position. 155 | */ 156 | readableRange(from: number, to: number) { 157 | const lcf = this.sourceFile.getLineAndCharacterOfPosition(from); 158 | const lct = this.sourceFile.getLineAndCharacterOfPosition(to); 159 | if (lcf.line === lct.line && lcf.character === lct.character) { 160 | return `at line ${lcf.line + 1}, char ${lcf.character + 1}`; 161 | } else { 162 | return `'${this.sourceFile.text 163 | .substring(from, to) 164 | .replace(/\n/g, '\\n')}'`; 165 | } 166 | } 167 | } 168 | 169 | /** 170 | * A `Fix` is a potential repair to the associated `Failure`. 171 | */ 172 | export interface Fix { 173 | /** 174 | * The individual text replacements composing that fix. 175 | */ 176 | changes: IndividualChange[]; 177 | } 178 | 179 | /** Creates a fix that replaces the given node with the new text. */ 180 | export function replaceNode(node: ts.Node, replacement: string): Fix { 181 | return { 182 | changes: [ 183 | { 184 | sourceFile: node.getSourceFile(), 185 | start: node.getStart(), 186 | end: node.getEnd(), 187 | replacement, 188 | }, 189 | ], 190 | }; 191 | } 192 | 193 | /** Creates a fix that inserts new text in front of the given node. */ 194 | export function insertBeforeNode(node: ts.Node, insertion: string): Fix { 195 | return { 196 | changes: [ 197 | { 198 | sourceFile: node.getSourceFile(), 199 | start: node.getStart(), 200 | end: node.getStart(), 201 | replacement: insertion, 202 | }, 203 | ], 204 | }; 205 | } 206 | 207 | /** 208 | * An individual text replacement/insertion in a source file. Used as part of a 209 | * `Fix`. 210 | */ 211 | export interface IndividualChange { 212 | sourceFile: ts.SourceFile; 213 | start: number; 214 | end: number; 215 | replacement: string; 216 | } 217 | 218 | /** 219 | * A ts.Diagnostic that might include fixes, and with an added `end` 220 | * field for convenience. 221 | */ 222 | export interface DiagnosticWithFixes extends ts.Diagnostic { 223 | end: number; 224 | /** 225 | * An array of fixes for a given diagnostic. 226 | * 227 | * Each element (fix) of the array provides a different alternative on how to 228 | * fix the diagnostic. Every fix is self contained and indepedent to other 229 | * fixes in the array. 230 | * 231 | * These fixes can be integrated into IDEs and presented to the users who can 232 | * choose the most suitable fix. 233 | */ 234 | fixes: Fix[]; 235 | } 236 | 237 | /** 238 | * Stringifies a `Fix`, replacing the `ts.SourceFile` with the matching 239 | * filename. 240 | */ 241 | export function fixToString(f?: Fix) { 242 | if (!f) return 'undefined'; 243 | return ( 244 | '{' + 245 | JSON.stringify( 246 | f.changes.map((ic) => { 247 | return { 248 | start: ic.start, 249 | end: ic.end, 250 | replacement: ic.replacement, 251 | fileName: ic.sourceFile.fileName, 252 | }; 253 | }), 254 | ) + 255 | '}' 256 | ); 257 | } 258 | -------------------------------------------------------------------------------- /common/third_party/tsetse/rule.ts: -------------------------------------------------------------------------------- 1 | import {Checker} from './checker'; 2 | 3 | /** 4 | * Tsetse rules should extend AbstractRule and provide a `register` function. 5 | * Rules are instantiated once per compilation operation and used across many 6 | * files. 7 | */ 8 | export abstract class AbstractRule { 9 | /** 10 | * A lower-dashed-case name for that rule. This is not used by Tsetse itself, 11 | * but the integrators might (such as the TypeScript Bazel rules, for 12 | * instance). 13 | */ 14 | abstract readonly ruleName: string; 15 | abstract readonly code: number; 16 | 17 | /** 18 | * Registers handler functions on nodes in Checker. 19 | */ 20 | abstract register(checker: Checker): void; 21 | } 22 | -------------------------------------------------------------------------------- /common/third_party/tsetse/rules/check_side_effect_import_rule.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import {Checker} from '../checker'; 4 | import {ErrorCode} from '../error_code'; 5 | import {AbstractRule} from '../rule'; 6 | 7 | function checkImport(checker: Checker, node: ts.ImportDeclaration) { 8 | // Check only side-effect imports as other imports are checked by TSC. 9 | if (node.importClause !== undefined) return; 10 | 11 | const modSpec = node.moduleSpecifier; 12 | if (!modSpec) return; 13 | 14 | // Code with syntax errors can cause moduleSpecifier to be something other 15 | // than a string literal. Early return to avoid a crash when 16 | // `moduleSpecifier.name` is undefined. 17 | if (!ts.isStringLiteral(modSpec)) return; 18 | 19 | const sym = checker.typeChecker.getSymbolAtLocation(modSpec); 20 | if (sym) return; 21 | 22 | // It is possible that getSymbolAtLocation returns undefined, but module name 23 | // is actually resolvable - e.g. when the imported file is empty (i.e. it is a 24 | // script, not a module). Therefore we have to check with resolveModuleName. 25 | 26 | const modName = modSpec.text; 27 | const source = node.getSourceFile(); 28 | const resolvingResult = checker.resolveModuleName(modName, source); 29 | if (resolvingResult && resolvingResult.resolvedModule) return; 30 | 31 | checker.addFailureAtNode( 32 | node, `Cannot find module`, /*source=*/ undefined, 33 | /*allowlist*/ undefined); 34 | } 35 | 36 | /** 37 | * Checks side effects imports and adds failures on module resolution errors. 38 | * This is an equivalent of the TS2307 error, but for side effects imports. 39 | */ 40 | export class Rule extends AbstractRule { 41 | static readonly RULE_NAME = 'check-imports'; 42 | readonly ruleName = Rule.RULE_NAME; 43 | readonly code = ErrorCode.BAD_SIDE_EFFECT_IMPORT; 44 | 45 | register(checker: Checker) { 46 | checker.on(ts.SyntaxKind.ImportDeclaration, checkImport, this.code); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /common/third_party/tsetse/rules/conformance_pattern_rule.ts: -------------------------------------------------------------------------------- 1 | import {Checker} from '../checker'; 2 | import {ErrorCode} from '../error_code'; 3 | import {AbstractRule} from '../rule'; 4 | import {Fixer} from '../util/fixer'; 5 | import {PatternEngineConfig, PatternKind, PatternRuleConfig} from '../util/pattern_config'; 6 | import {ImportedNameEngine, NameEngine} from '../util/pattern_engines/name_engine'; 7 | import {PatternEngine} from '../util/pattern_engines/pattern_engine'; 8 | import {PropertyEngine} from '../util/pattern_engines/property_engine'; 9 | import {PropertyNonConstantWriteEngine} from '../util/pattern_engines/property_non_constant_write_engine'; 10 | import {PropertyWriteEngine} from '../util/pattern_engines/property_write_engine'; 11 | 12 | /** 13 | * Builds a Rule that matches a certain pattern, given as parameter, and 14 | * that can additionally run a suggested fix generator on the matches. 15 | * 16 | * This is templated, mostly to ensure the nodes that have been matched 17 | * correspond to what the Fixer expects. 18 | */ 19 | export class ConformancePatternRule implements AbstractRule { 20 | readonly ruleName: string; 21 | readonly code: number; 22 | private readonly engine: PatternEngine; 23 | 24 | constructor(readonly config: PatternRuleConfig, fixers?: Fixer[]) { 25 | this.code = config.errorCode; 26 | // Avoid undefined rule names. 27 | this.ruleName = config.name ?? ''; 28 | 29 | let engine: { 30 | new (ruleName: string, config: PatternEngineConfig, fixers?: Fixer[]): 31 | PatternEngine 32 | }; 33 | 34 | switch (config.kind) { 35 | case PatternKind.BANNED_PROPERTY: 36 | engine = PropertyEngine; 37 | break; 38 | case PatternKind.BANNED_PROPERTY_WRITE: 39 | engine = PropertyWriteEngine; 40 | break; 41 | case PatternKind.BANNED_PROPERTY_NON_CONSTANT_WRITE: 42 | engine = PropertyNonConstantWriteEngine; 43 | break; 44 | case PatternKind.BANNED_NAME: 45 | engine = NameEngine; 46 | break; 47 | case PatternKind.BANNED_IMPORTED_NAME: 48 | engine = ImportedNameEngine; 49 | break; 50 | default: 51 | throw new Error('Config type not recognized, or not implemented yet.'); 52 | } 53 | 54 | this.engine = new engine(this.ruleName, config, fixers); 55 | } 56 | 57 | register(checker: Checker) { 58 | this.engine.register(checker); 59 | } 60 | } 61 | 62 | // Re-exported for convenience when instantiating rules. 63 | /** 64 | * The list of supported patterns useable in ConformancePatternRule. The 65 | * patterns whose name match JSConformance patterns should behave similarly (see 66 | * https://github.com/google/closure-compiler/wiki/JS-Conformance-Framework). 67 | */ 68 | export {PatternKind}; 69 | export {ErrorCode}; 70 | -------------------------------------------------------------------------------- /common/third_party/tsetse/util/ast_tools.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview This is a collection of smaller utility functions to operate on 3 | * a TypeScript AST, used by JSConformance rules and elsewhere. 4 | */ 5 | 6 | import * as ts from 'typescript'; 7 | 8 | /** 9 | * Triggers increased verbosity in the rules. 10 | */ 11 | let DEBUG = false; 12 | 13 | /** 14 | * Turns on or off logging for ConformancePatternRules. 15 | */ 16 | export function setDebug(state: boolean) { 17 | DEBUG = state; 18 | } 19 | 20 | /** 21 | * Debug helper. 22 | */ 23 | export function debugLog(msg: () => string) { 24 | if (DEBUG) { 25 | console.log(msg()); 26 | } 27 | } 28 | 29 | /** 30 | * Returns `n`'s parents in order. 31 | */ 32 | export function parents(n: ts.Node): ts.Node[] { 33 | const p = []; 34 | while (n.parent) { 35 | n = n.parent; 36 | p.push(n); 37 | } 38 | return p; 39 | } 40 | 41 | /** 42 | * Searches for something satisfying the given test in `n` or its children. 43 | */ 44 | export function findInChildren( 45 | n: ts.Node, 46 | test: (n: ts.Node) => boolean, 47 | ): boolean { 48 | let toExplore: ts.Node[] = [n]; 49 | let cur: ts.Node | undefined; 50 | while ((cur = toExplore.pop())) { 51 | if (test(cur)) { 52 | return true; 53 | } 54 | // Recurse 55 | toExplore = toExplore.concat(cur.getChildren()); 56 | } 57 | return false; 58 | } 59 | 60 | function isOperandOfInstanceOf(n: ts.Node) { 61 | return ( 62 | ts.isBinaryExpression(n.parent) && 63 | n.parent.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword 64 | ); 65 | } 66 | 67 | /** 68 | * Returns true if the pattern-based Rule should look at that node and consider 69 | * warning there. 70 | */ 71 | export function shouldExamineNode(n: ts.Node) { 72 | return !( 73 | (n.parent && ts.isTypeNode(n.parent)) || 74 | isOperandOfInstanceOf(n) || 75 | ts.isTypeOfExpression(n.parent) || 76 | isInStockLibraries(n) 77 | ); 78 | } 79 | 80 | /** 81 | * Return whether the given Node is (or is in) a library included as default. 82 | * We currently look for a node_modules/typescript/ prefix, but this could 83 | * be expanded if needed. 84 | */ 85 | export function isInStockLibraries(n: ts.Node | ts.SourceFile): boolean { 86 | const sourceFile = ts.isSourceFile(n) ? n : n.getSourceFile(); 87 | if (sourceFile) { 88 | return sourceFile.fileName.indexOf('node_modules/typescript/') !== -1; 89 | } else { 90 | // the node is nowhere? Consider it as part of the core libs: we can't 91 | // do anything with it anyways, and it was likely included as default. 92 | return true; 93 | } 94 | } 95 | 96 | /** 97 | * Turns the given Symbol into its non-aliased version (which could be itself). 98 | * Returns undefined if given an undefined Symbol (so you can call 99 | * `dealias(typeChecker.getSymbolAtLocation(node))`). 100 | */ 101 | export function dealias( 102 | symbol: ts.Symbol | undefined, 103 | tc: ts.TypeChecker, 104 | ): ts.Symbol | undefined { 105 | if (!symbol) { 106 | return undefined; 107 | } 108 | if (symbol.getFlags() & ts.SymbolFlags.Alias) { 109 | // Note: something that has only TypeAlias is not acceptable here. 110 | return dealias(tc.getAliasedSymbol(symbol), tc); 111 | } 112 | return symbol; 113 | } 114 | 115 | /** 116 | * Returns whether `n`'s parents are something indicating a type. 117 | */ 118 | export function isPartOfTypeDeclaration(n: ts.Node) { 119 | return [n, ...parents(n)].some( 120 | (p) => 121 | p.kind === ts.SyntaxKind.TypeReference || 122 | p.kind === ts.SyntaxKind.TypeLiteral, 123 | ); 124 | } 125 | 126 | /** 127 | * Returns whether `n` is a declared name on which we do not intend to emit 128 | * errors. 129 | */ 130 | export function isAllowlistedNamedDeclaration( 131 | n: ts.Node, 132 | ): n is 133 | | ts.VariableDeclaration 134 | | ts.ClassDeclaration 135 | | ts.FunctionDeclaration 136 | | ts.MethodDeclaration 137 | | ts.PropertyDeclaration 138 | | ts.InterfaceDeclaration 139 | | ts.TypeAliasDeclaration 140 | | ts.EnumDeclaration 141 | | ts.ModuleDeclaration 142 | | ts.ImportEqualsDeclaration 143 | | ts.ExportDeclaration 144 | | ts.MissingDeclaration 145 | | ts.ImportClause 146 | | ts.ExportSpecifier 147 | | ts.ImportSpecifier { 148 | return ( 149 | ts.isVariableDeclaration(n) || 150 | ts.isClassDeclaration(n) || 151 | ts.isFunctionDeclaration(n) || 152 | ts.isMethodDeclaration(n) || 153 | ts.isPropertyDeclaration(n) || 154 | ts.isInterfaceDeclaration(n) || 155 | ts.isTypeAliasDeclaration(n) || 156 | ts.isEnumDeclaration(n) || 157 | ts.isModuleDeclaration(n) || 158 | ts.isImportEqualsDeclaration(n) || 159 | ts.isExportDeclaration(n) || 160 | ts.isMissingDeclaration(n) || 161 | ts.isImportClause(n) || 162 | ts.isExportSpecifier(n) || 163 | ts.isImportSpecifier(n) 164 | ); 165 | } 166 | 167 | /** 168 | * If verbose, logs the given error that happened while walking n, with a 169 | * stacktrace. 170 | */ 171 | export function logASTWalkError(verbose: boolean, n: ts.Node, e: Error) { 172 | let nodeText = `[error getting name for ${JSON.stringify(n)}]`; 173 | try { 174 | nodeText = '"' + n.getFullText().trim() + '"'; 175 | } catch {} 176 | debugLog( 177 | () => 178 | `Walking node ${nodeText} failed with error ${e}.\n` + 179 | `Stacktrace:\n${e.stack}`, 180 | ); 181 | } 182 | -------------------------------------------------------------------------------- /common/third_party/tsetse/util/ban_jsdoc.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import {Allowlist} from '../allowlist'; 4 | import {Checker} from '../checker'; 5 | import {replaceNode} from '../failure'; 6 | 7 | /** 8 | * All the ts.SyntaxKind members, which can have JSDoc tags according to 9 | * ts.HasJSDoc. A type test below verifies that this array stays up to date. 10 | */ 11 | export const HAS_JSDOC_SYNTAX_KINDS = [ 12 | ts.SyntaxKind.ArrowFunction, 13 | ts.SyntaxKind.Block, 14 | ts.SyntaxKind.BreakStatement, 15 | ts.SyntaxKind.CallSignature, 16 | ts.SyntaxKind.CaseClause, 17 | ts.SyntaxKind.ClassDeclaration, 18 | ts.SyntaxKind.ClassExpression, 19 | ts.SyntaxKind.ClassStaticBlockDeclaration, 20 | ts.SyntaxKind.ConstructSignature, 21 | ts.SyntaxKind.Constructor, 22 | ts.SyntaxKind.ConstructorType, 23 | ts.SyntaxKind.ContinueStatement, 24 | ts.SyntaxKind.DebuggerStatement, 25 | ts.SyntaxKind.DoStatement, 26 | ts.SyntaxKind.EmptyStatement, 27 | ts.SyntaxKind.EndOfFileToken, 28 | ts.SyntaxKind.EnumDeclaration, 29 | ts.SyntaxKind.EnumMember, 30 | ts.SyntaxKind.ExportAssignment, 31 | ts.SyntaxKind.ExportDeclaration, 32 | ts.SyntaxKind.ExportSpecifier, 33 | ts.SyntaxKind.ExpressionStatement, 34 | ts.SyntaxKind.ForInStatement, 35 | ts.SyntaxKind.ForOfStatement, 36 | ts.SyntaxKind.ForStatement, 37 | ts.SyntaxKind.FunctionDeclaration, 38 | ts.SyntaxKind.FunctionExpression, 39 | ts.SyntaxKind.FunctionType, 40 | ts.SyntaxKind.GetAccessor, 41 | ts.SyntaxKind.IfStatement, 42 | ts.SyntaxKind.ImportDeclaration, 43 | ts.SyntaxKind.ImportEqualsDeclaration, 44 | ts.SyntaxKind.IndexSignature, 45 | ts.SyntaxKind.InterfaceDeclaration, 46 | ts.SyntaxKind.JSDocFunctionType, 47 | ts.SyntaxKind.LabeledStatement, 48 | ts.SyntaxKind.MethodDeclaration, 49 | ts.SyntaxKind.MethodSignature, 50 | ts.SyntaxKind.ModuleDeclaration, 51 | ts.SyntaxKind.NamedTupleMember, 52 | ts.SyntaxKind.NamespaceExportDeclaration, 53 | ts.SyntaxKind.Parameter, 54 | ts.SyntaxKind.ParenthesizedExpression, 55 | ts.SyntaxKind.PropertyAssignment, 56 | ts.SyntaxKind.PropertyDeclaration, 57 | ts.SyntaxKind.PropertySignature, 58 | ts.SyntaxKind.ReturnStatement, 59 | ts.SyntaxKind.SetAccessor, 60 | ts.SyntaxKind.ShorthandPropertyAssignment, 61 | ts.SyntaxKind.SpreadAssignment, 62 | ts.SyntaxKind.SwitchStatement, 63 | ts.SyntaxKind.ThrowStatement, 64 | ts.SyntaxKind.TryStatement, 65 | ts.SyntaxKind.TypeAliasDeclaration, 66 | ts.SyntaxKind.VariableDeclaration, 67 | ts.SyntaxKind.VariableStatement, 68 | ts.SyntaxKind.WhileStatement, 69 | ts.SyntaxKind.WithStatement, 70 | ] as const; 71 | 72 | // Verify that the `HAS_JSDOC_SYNTAX_KINDS` array stays up to date. If you see a 73 | // compile error here, add syntax kinds to the array or remove them to make it 74 | // align with `ts.HasJSDoc` again. 75 | type LocalJSDocSyntaxKind = (typeof HAS_JSDOC_SYNTAX_KINDS)[number]; 76 | type ExpectLocalArrayToContainAllOf = T; 77 | type ExpectTsDeclarationToContainAllOf = T; 78 | // tslint:disable-next-line:no-unused-variable 79 | type TestCases = [ 80 | // TODO: go/ts50upgrade - Auto-added; fix after TS 5.0 upgrade. 81 | // TS2344: Type 'SyntaxKind.EndOfFileToken | SyntaxKind.Identifier | 82 | // SyntaxKind.TypeParameter | SyntaxKind.Parameter | 83 | // SyntaxKind.PropertySignature | SyntaxKind.PropertyDeclaration | 84 | // SyntaxKind.MethodSignature | ... 57 more ... | S... 85 | // @ts-ignore 86 | ExpectLocalArrayToContainAllOf, 87 | ExpectTsDeclarationToContainAllOf, 88 | ]; 89 | 90 | /** 91 | * Shared logic for checking for banned JSDoc tags. Create rules that use this 92 | * for each banned tag, which will allow you to use standard allowlist 93 | * functionality per tag. 94 | */ 95 | export function checkForBannedJsDocTag( 96 | bannedTag: string, 97 | ruleName: string, 98 | checker: Checker, 99 | node: ts.HasJSDoc, 100 | allowlist?: Allowlist, 101 | ) { 102 | const allTags: ts.JSDocTag[] = []; 103 | for (const child of node.getChildren()) { 104 | if (!ts.isJSDoc(child) || !child.tags) continue; 105 | allTags.push(...child.tags); 106 | } 107 | const internalTag = allTags.find( 108 | (tag) => tag.tagName.getText() === bannedTag, 109 | ); 110 | if (!internalTag) return; 111 | 112 | const fix = replaceNode(internalTag, ''); 113 | checker.addFailureAtNode( 114 | internalTag, 115 | `do not use @${bannedTag}`, 116 | ruleName, 117 | allowlist, 118 | [fix], 119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /common/third_party/tsetse/util/fixer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import {Fix, IndividualChange} from '../failure'; 4 | import {debugLog} from './ast_tools'; 5 | 6 | export {type Fix} from '../failure'; 7 | 8 | /** 9 | * A Fixer turns Nodes (that are supposed to have been matched before) into a 10 | * Fix. This is meant to be implemented by Rule implementers (or 11 | * ban-preset-pattern users). See also `buildReplacementFixer` for a simpler way 12 | * of implementing a Fixer. 13 | */ 14 | export interface Fixer { 15 | getFixForFlaggedNode(node: ts.Node): Fix | undefined; 16 | } 17 | 18 | /** 19 | * A simple Fixer builder based on a function that looks at a node, and 20 | * output either nothing, or a replacement. If this is too limiting, implement 21 | * Fixer instead. 22 | */ 23 | export function buildReplacementFixer( 24 | potentialReplacementGenerator: ( 25 | node: ts.Node, 26 | ) => {replaceWith: string} | undefined, 27 | ): Fixer { 28 | return { 29 | getFixForFlaggedNode: (n: ts.Node): Fix | undefined => { 30 | const partialFix = potentialReplacementGenerator(n); 31 | if (!partialFix) { 32 | return; 33 | } 34 | return { 35 | changes: [ 36 | { 37 | sourceFile: n.getSourceFile(), 38 | start: n.getStart(), 39 | end: n.getEnd(), 40 | replacement: partialFix.replaceWith, 41 | }, 42 | ], 43 | }; 44 | }, 45 | }; 46 | } 47 | 48 | interface NamedImportsFromModule { 49 | namedBindings: ts.NamedImports; 50 | fromModule: string; 51 | } 52 | 53 | interface NamespaceImportFromModule { 54 | namedBindings: ts.NamespaceImport; 55 | fromModule: string; 56 | } 57 | 58 | // Type union is not distributive over properties so just define a new inteface 59 | interface NamedImportBindingsFromModule { 60 | namedBindings: ts.NamedImports | ts.NamespaceImport; 61 | fromModule: string; 62 | } 63 | 64 | /** 65 | * Builds an IndividualChange that imports the required symbol from the given 66 | * file under the given name. This might reimport the same thing twice in some 67 | * cases, but it will always make it available under the right name (though 68 | * its name might collide with other imports, as we don't currently check for 69 | * that). 70 | */ 71 | export function maybeAddNamedImport( 72 | source: ts.SourceFile, 73 | importWhat: string, 74 | fromModule: string, 75 | importAs?: string, 76 | tazeComment?: string, 77 | ): IndividualChange | undefined { 78 | const importStatements = source.statements.filter(ts.isImportDeclaration); 79 | const importSpecifier = importAs 80 | ? `${importWhat} as ${importAs}` 81 | : importWhat; 82 | 83 | // See if the original code already imported something from the right file 84 | const importFromRightModule = importStatements 85 | .map(maybeParseImportNode) 86 | // Exclude undefined 87 | .filter( 88 | (binding): binding is NamedImportBindingsFromModule => 89 | binding !== undefined, 90 | ) 91 | // Exclude wildcard import 92 | .filter((binding): binding is NamedImportsFromModule => 93 | ts.isNamedImports(binding.namedBindings), 94 | ) 95 | // Exlcude imports from the wrong file 96 | .find((binding) => binding.fromModule === fromModule); 97 | 98 | if (importFromRightModule) { 99 | const foundRightImport = importFromRightModule.namedBindings.elements.some( 100 | (iSpec) => 101 | iSpec.propertyName 102 | ? iSpec.name.getText() === importAs && // import {foo as bar} 103 | iSpec.propertyName.getText() === importWhat 104 | : iSpec.name.getText() === importWhat, 105 | ); // import {foo} 106 | if (!foundRightImport) { 107 | // Insert our symbol in the list of imports from that file. 108 | debugLog(() => `No named imports from that file, generating new fix`); 109 | return { 110 | start: importFromRightModule.namedBindings.elements[0].getStart(), 111 | end: importFromRightModule.namedBindings.elements[0].getStart(), 112 | sourceFile: source, 113 | replacement: `${importSpecifier}, `, 114 | }; 115 | } 116 | return; // Our request is already imported under the right name. 117 | } 118 | 119 | // If we get here, we didn't find anything imported from the wanted file, so 120 | // we'll need the full import string. Add it after the last import, 121 | // and let clang-format handle the rest. 122 | const newImportStatement = 123 | `import {${importSpecifier}} from '${fromModule}';` + 124 | (tazeComment ? ` ${tazeComment}\n` : `\n`); 125 | const insertionPosition = importStatements.length 126 | ? importStatements[importStatements.length - 1].getEnd() + 1 127 | : 0; 128 | return { 129 | start: insertionPosition, 130 | end: insertionPosition, 131 | sourceFile: source, 132 | replacement: newImportStatement, 133 | }; 134 | } 135 | 136 | /** 137 | * Builds an IndividualChange that imports the required namespace from the given 138 | * file under the given name. This might reimport the same thing twice in some 139 | * cases, but it will always make it available under the right name (though 140 | * its name might collide with other imports, as we don't currently check for 141 | * that). 142 | */ 143 | export function maybeAddNamespaceImport( 144 | source: ts.SourceFile, 145 | fromModule: string, 146 | importAs: string, 147 | tazeComment?: string, 148 | ): IndividualChange | undefined { 149 | const importStatements = source.statements.filter(ts.isImportDeclaration); 150 | 151 | const hasTheRightImport = importStatements 152 | .map(maybeParseImportNode) 153 | // Exclude undefined 154 | .filter( 155 | (binding): binding is NamedImportBindingsFromModule => 156 | binding !== undefined, 157 | ) 158 | // Exlcude named imports 159 | .filter((binding): binding is NamespaceImportFromModule => 160 | ts.isNamespaceImport(binding.namedBindings), 161 | ) 162 | .some( 163 | (binding) => 164 | binding.fromModule === fromModule && 165 | binding.namedBindings.name.getText() === importAs, 166 | ); 167 | 168 | if (!hasTheRightImport) { 169 | const insertionPosition = importStatements.length 170 | ? importStatements[importStatements.length - 1].getEnd() + 1 171 | : 0; 172 | return { 173 | start: insertionPosition, 174 | end: insertionPosition, 175 | sourceFile: source, 176 | replacement: tazeComment 177 | ? `import * as ${importAs} from '${fromModule}'; ${tazeComment}\n` 178 | : `import * as ${importAs} from '${fromModule}';\n`, 179 | }; 180 | } 181 | return; 182 | } 183 | 184 | /** 185 | * This tries to make sense of an ImportDeclaration, and returns the 186 | * interesting parts, undefined if the import declaration is valid but not 187 | * understandable by the checker. 188 | */ 189 | function maybeParseImportNode( 190 | iDecl: ts.ImportDeclaration, 191 | ): NamedImportBindingsFromModule | undefined { 192 | if (!iDecl.importClause) { 193 | // something like import "./file"; 194 | debugLog( 195 | () => `Ignoring import without imported symbol: ${iDecl.getFullText()}`, 196 | ); 197 | return; 198 | } 199 | if (iDecl.importClause.name || !iDecl.importClause.namedBindings) { 200 | // Seems to happen in defaults imports like import Foo from 'Bar'. 201 | // Not much we can do with that when trying to get a hold of some 202 | // symbols, so just ignore that line (worst case, we'll suggest another 203 | // import style). 204 | debugLog(() => `Ignoring import: ${iDecl.getFullText()}`); 205 | return; 206 | } 207 | if (!ts.isStringLiteral(iDecl.moduleSpecifier)) { 208 | debugLog(() => `Ignoring import whose module specifier is not literal`); 209 | return; 210 | } 211 | return { 212 | namedBindings: iDecl.importClause.namedBindings, 213 | fromModule: iDecl.moduleSpecifier.text, 214 | }; 215 | } 216 | -------------------------------------------------------------------------------- /common/third_party/tsetse/util/is_expression_value_used_or_void.ts: -------------------------------------------------------------------------------- 1 | import * as tsutils from 'tsutils'; 2 | import * as ts from 'typescript'; 3 | 4 | /** 5 | * Checks whether an expression value is used, or whether it is the operand of a 6 | * void expression. 7 | * 8 | * This allows the `void` operator to be used to intentionally suppress 9 | * conformance checks. 10 | */ 11 | export function isExpressionValueUsedOrVoid(node: ts.CallExpression) { 12 | return ( 13 | ts.isVoidExpression(node.parent) || tsutils.isExpressionValueUsed(node) 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /common/third_party/tsetse/util/is_literal.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import {findInChildren} from './ast_tools'; 3 | 4 | /** 5 | * Determines if the given ts.Node is literal enough for security purposes. This 6 | * is true when the value is built from compile-time constants, with a certain 7 | * tolerance for indirection in order to make this more user-friendly. 8 | * 9 | * This considers a few different things. We accept 10 | * - What TS deems literal (literal strings, literal numbers, literal booleans, 11 | * enum literals), 12 | * - Binary operations of two expressions that we accept (including 13 | * concatenation), 14 | * - Template interpolations of what we accept, 15 | * - `x?y:z` constructions, if we accept `y` and `z` 16 | * - Variables that are const, and initialized with an expression we accept 17 | * 18 | * And to prevent bypasses, expressions that include casts are not accepted. 19 | */ 20 | export function isLiteral(typeChecker: ts.TypeChecker, node: ts.Node): boolean { 21 | if ( 22 | ts.isBinaryExpression(node) && 23 | node.operatorToken.kind === ts.SyntaxKind.PlusToken 24 | ) { 25 | // Concatenation is fine, if the parts are literals. 26 | return ( 27 | isLiteral(typeChecker, node.left) && isLiteral(typeChecker, node.right) 28 | ); 29 | } else if (ts.isTemplateExpression(node)) { 30 | // Same for template expressions. 31 | return node.templateSpans.every((span) => { 32 | return isLiteral(typeChecker, span.expression); 33 | }); 34 | } else if (ts.isTemplateLiteral(node)) { 35 | // and literals (in that order). 36 | return true; 37 | } else if (ts.isConditionalExpression(node)) { 38 | return ( 39 | isLiteral(typeChecker, node.whenTrue) && 40 | isLiteral(typeChecker, node.whenFalse) 41 | ); 42 | } else if (ts.isIdentifier(node)) { 43 | return isUnderlyingValueAStringLiteral(node, typeChecker); 44 | } 45 | 46 | const hasCasts = findInChildren( 47 | node, 48 | (n) => ts.isAsExpression(n) && !ts.isConstTypeReference(n.type), 49 | ); 50 | 51 | return !hasCasts && isLiteralAccordingToItsType(typeChecker, node); 52 | } 53 | 54 | /** 55 | * Given an identifier, this function goes around the AST to determine 56 | * whether we should consider it a string literal, on a best-effort basis. It 57 | * is an approximation, but should never have false positives. 58 | */ 59 | function isUnderlyingValueAStringLiteral( 60 | identifier: ts.Identifier, 61 | tc: ts.TypeChecker, 62 | ) { 63 | // The identifier references a value, and we try to follow the trail: if we 64 | // find a variable declaration for the identifier, and it was declared as a 65 | // const (so we know it wasn't altered along the way), then the value used 66 | // in the declaration is the value our identifier references. That means we 67 | // should look at the value used in its initialization (by applying the same 68 | // rules as before). 69 | // Since we're best-effort, if a part of that operation failed due to lack 70 | // of support, then we fail closed and don't consider the value a literal. 71 | const declarations = getDeclarations(identifier, tc); 72 | const variableDeclarations = declarations.filter(ts.isVariableDeclaration); 73 | if (variableDeclarations.length) { 74 | return variableDeclarations 75 | .filter(isConst) 76 | .some((d) => d.initializer !== undefined && isLiteral(tc, d.initializer)); 77 | } 78 | const importDeclarations = declarations.filter(ts.isImportSpecifier); 79 | if (importDeclarations.length) { 80 | return isLiteralAccordingToItsType(tc, identifier); 81 | } 82 | return false; 83 | } 84 | 85 | /** 86 | * Returns whether this thing is a literal based on TS's understanding. 87 | */ 88 | function isLiteralAccordingToItsType( 89 | typeChecker: ts.TypeChecker, 90 | node: ts.Node, 91 | ): boolean { 92 | const nodeType = typeChecker.getTypeAtLocation(node); 93 | return ( 94 | (nodeType.flags & 95 | (ts.TypeFlags.StringLiteral | 96 | ts.TypeFlags.NumberLiteral | 97 | ts.TypeFlags.BooleanLiteral | 98 | ts.TypeFlags.EnumLiteral)) !== 99 | 0 100 | ); 101 | } 102 | 103 | /** 104 | * Follows the symbol behind the given identifier, assuming it is a variable, 105 | * and return all the variable declarations we can find that match it in the 106 | * same file. 107 | */ 108 | function getDeclarations( 109 | node: ts.Identifier, 110 | tc: ts.TypeChecker, 111 | ): ts.Declaration[] { 112 | const symbol = tc.getSymbolAtLocation(node); 113 | if (!symbol) { 114 | return []; 115 | } 116 | return symbol.getDeclarations() ?? []; 117 | } 118 | 119 | // Tests whether the given variable declaration is Const. 120 | function isConst(varDecl: ts.VariableDeclaration): boolean { 121 | return Boolean( 122 | varDecl && 123 | varDecl.parent && 124 | ts.isVariableDeclarationList(varDecl.parent) && 125 | varDecl.parent.flags & ts.NodeFlags.Const, 126 | ); 127 | } 128 | -------------------------------------------------------------------------------- /common/third_party/tsetse/util/is_trusted_type.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import {debugLog} from './ast_tools'; 4 | import {TrustedTypesConfig} from './trusted_types_configuration'; 5 | 6 | /** 7 | * Returns true if the AST expression is Trusted Types compliant and can be 8 | * safely used in the sink. 9 | * 10 | * This function is only called if the rule is configured to allow specific 11 | * Trusted Type value in the assignment. 12 | */ 13 | export function isExpressionOfAllowedTrustedType( 14 | tc: ts.TypeChecker, 15 | expr: ts.Expression, 16 | allowedType: TrustedTypesConfig, 17 | ): boolean { 18 | if (isTrustedType(tc, expr, allowedType)) return true; 19 | if (isTrustedTypeCastToUnknownToString(tc, expr, allowedType)) return true; 20 | if (isTrustedTypeUnionWithStringCastToString(tc, expr, allowedType)) { 21 | return true; 22 | } 23 | if (isTrustedTypeUnwrapperFunction(tc, expr, allowedType)) return true; 24 | return false; 25 | } 26 | 27 | /** 28 | * Helper function which checks if given TS Symbol is allowed (matches 29 | * configured Trusted Type). 30 | */ 31 | function isAllowedSymbol( 32 | tc: ts.TypeChecker, 33 | symbol: ts.Symbol | undefined, 34 | allowedType: TrustedTypesConfig, 35 | allowAmbientTrustedTypesDeclaration: boolean, 36 | ) { 37 | debugLog(() => `isAllowedSymbol called with symbol ${symbol?.getName()}`); 38 | if (!symbol) return false; 39 | 40 | const fqn = tc.getFullyQualifiedName(symbol); 41 | debugLog(() => `fully qualified name is ${fqn}`); 42 | if ( 43 | allowAmbientTrustedTypesDeclaration && 44 | allowedType.allowAmbientTrustedTypesDeclaration && 45 | fqn === allowedType.typeName 46 | ) { 47 | return true; 48 | } 49 | 50 | if (!fqn.endsWith('.' + allowedType.typeName)) return false; 51 | 52 | // check that the type is comes allowed declaration file 53 | const declarations = symbol.getDeclarations(); 54 | if (!declarations) return false; 55 | const declarationFileNames = declarations.map( 56 | (d) => d.getSourceFile()?.fileName, 57 | ); 58 | debugLog(() => `got declaration filenames ${declarationFileNames}`); 59 | 60 | return declarationFileNames.some((fileName) => 61 | fileName.includes(allowedType.modulePathMatcher), 62 | ); 63 | } 64 | 65 | /** 66 | * Returns true if the expression matches the following format: 67 | * "AllowedTrustedType as unknown as string" 68 | */ 69 | function isTrustedTypeCastToUnknownToString( 70 | tc: ts.TypeChecker, 71 | expr: ts.Expression, 72 | allowedType: TrustedTypesConfig, 73 | ) { 74 | // check if the expression is a cast expression to string 75 | if ( 76 | !ts.isAsExpression(expr) || 77 | expr.type.kind !== ts.SyntaxKind.StringKeyword 78 | ) { 79 | return false; 80 | } 81 | 82 | // inner expression should be another cast expression 83 | const innerExpr = expr.expression; 84 | if ( 85 | !ts.isAsExpression(innerExpr) || 86 | innerExpr.type.kind !== ts.SyntaxKind.UnknownKeyword 87 | ) { 88 | return false; 89 | } 90 | 91 | // check if left side of the cast expression is of an allowed type. 92 | const castSource = innerExpr.expression; 93 | debugLog(() => `looking at cast source ${castSource.getText()}`); 94 | return isAllowedSymbol( 95 | tc, 96 | tc.getTypeAtLocation(castSource).getSymbol(), 97 | allowedType, 98 | false, 99 | ); 100 | } 101 | 102 | /** 103 | * Returns true if the expression matches the following format: 104 | * "(AllowedTrustedType | string) as string" 105 | */ 106 | function isTrustedTypeUnionWithStringCastToString( 107 | tc: ts.TypeChecker, 108 | expr: ts.Expression, 109 | allowedType: TrustedTypesConfig, 110 | ) { 111 | // verify that the expression is a cast expression to string 112 | if ( 113 | !ts.isAsExpression(expr) || 114 | expr.type.kind !== ts.SyntaxKind.StringKeyword 115 | ) { 116 | return false; 117 | } 118 | 119 | // inner expression needs to be a type union of trusted value 120 | const innerExprType = tc.getTypeAtLocation(expr.expression); 121 | return ( 122 | innerExprType.isUnion() && 123 | // do not care how many types are in the union. As long as one of them is 124 | // the configured Trusted type we are happy. 125 | innerExprType.types.some((type) => 126 | isAllowedSymbol(tc, type.getSymbol(), allowedType, false), 127 | ) 128 | ); 129 | } 130 | 131 | /** 132 | * Returns true if the expression is a function call of the following signature: 133 | * "(TypeCompatibleWithTrustedType): string" 134 | * 135 | * where `TypeCompatibleWithTrustedType` can be either the Trusted Type itself 136 | * or a TS union. We only require the first argument of the call site to be 137 | * exact Trusted Type. Pattern like `unwrap('err msg', TT)` will not work. 138 | * We intentionally want make the unwrapper pattern more apparent by forcing the 139 | * TT value in the first argument. 140 | */ 141 | function isTrustedTypeUnwrapperFunction( 142 | tc: ts.TypeChecker, 143 | expr: ts.Expression, 144 | allowedType: TrustedTypesConfig, 145 | ) { 146 | if (!ts.isCallExpression(expr)) return false; 147 | 148 | return ( 149 | expr.arguments.length > 0 && 150 | isAllowedSymbol( 151 | tc, 152 | tc.getTypeAtLocation(expr.arguments[0]).getSymbol(), 153 | allowedType, 154 | false, 155 | ) 156 | ); 157 | } 158 | 159 | /** 160 | * Returns true if the expression is a value of Trusted Types, or a type that is 161 | * the intersection of Trusted Types and other types. 162 | */ 163 | function isTrustedType( 164 | tc: ts.TypeChecker, 165 | expr: ts.Expression, 166 | allowedType: TrustedTypesConfig, 167 | ) { 168 | const type = tc.getTypeAtLocation(expr); 169 | 170 | if (isAllowedSymbol(tc, type.getSymbol(), allowedType, true)) return true; 171 | 172 | if (!type.isIntersection()) return false; 173 | 174 | return type.types.some((t) => 175 | isAllowedSymbol(tc, t.getSymbol(), allowedType, true), 176 | ); 177 | } 178 | -------------------------------------------------------------------------------- /common/third_party/tsetse/util/pattern_config.ts: -------------------------------------------------------------------------------- 1 | import {AllowlistEntry} from '../allowlist'; 2 | import {TrustedTypesConfig} from './trusted_types_configuration'; 3 | 4 | /** 5 | * The list of supported patterns useable in ConformancePatternRule. The 6 | * patterns whose name match JSConformance patterns should behave similarly (see 7 | * https://github.com/google/closure-compiler/wiki/JS-Conformance-Framework) 8 | */ 9 | export enum PatternKind { 10 | /** Ban use of fully distinguished names. */ 11 | BANNED_NAME = 'banned-name', 12 | /** Ban use of fully distinguished names, even in import statements. */ 13 | BANNED_IMPORTED_NAME = 'banned-imported-name', 14 | /** Ban use of instance properties */ 15 | BANNED_PROPERTY = 'banned-property', 16 | /** 17 | * Ban instance property, like BANNED_PROPERTY but where reads of the 18 | * property are allowed. 19 | */ 20 | BANNED_PROPERTY_WRITE = 'banned-property-write', 21 | /** 22 | * Ban instance property write unless the property is assigned a constant 23 | * literal. 24 | */ 25 | BANNED_PROPERTY_NON_CONSTANT_WRITE = 'banned-property-non-constant-write', 26 | } 27 | 28 | /** 29 | * A config for `PatternEngine`. 30 | */ 31 | export interface PatternEngineConfig { 32 | /** 33 | * Values have a pattern-specific syntax. See each patternKind's tests for 34 | * examples. 35 | */ 36 | values: string[]; 37 | 38 | /** The error code assigned to this pattern. */ 39 | errorCode: number; 40 | 41 | /** The error message this pattern will create. */ 42 | errorMessage: string; 43 | 44 | /** A list of allowlist blocks. */ 45 | allowlistEntries?: AllowlistEntry[]; 46 | 47 | /** 48 | * Type of the allowed Trusted value by the rule or custom 49 | * `TrustedTypesConfig`. 50 | */ 51 | allowedTrustedType?: TrustedTypesConfig; 52 | } 53 | 54 | /** 55 | * A config for `ConformancePatternRule`. 56 | */ 57 | export interface PatternRuleConfig extends PatternEngineConfig { 58 | kind: PatternKind; 59 | 60 | /** 61 | * An optional name for that rule, which will be the rule's `ruleName`. 62 | * Should be lower-dashed-case. 63 | */ 64 | name?: string; 65 | } 66 | 67 | /** 68 | * Internal function to override the rule config properties before passing to 69 | * parent constructor. 70 | */ 71 | export function overridePatternConfig( 72 | config: PatternRuleConfig, 73 | ): PatternRuleConfig { 74 | 75 | return config; 76 | } 77 | -------------------------------------------------------------------------------- /common/third_party/tsetse/util/pattern_engines/name_engine.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import {Checker} from '../../checker'; 4 | import {AbsoluteMatcher} from '../absolute_matcher'; 5 | import {isExpressionOfAllowedTrustedType} from '../is_trusted_type'; 6 | import {TrustedTypesConfig} from '../trusted_types_configuration'; 7 | 8 | import {PatternEngine} from './pattern_engine'; 9 | 10 | function isCalledWithAllowedTrustedType( 11 | tc: ts.TypeChecker, 12 | n: ts.Node, 13 | allowedTrustedType: TrustedTypesConfig | undefined, 14 | ) { 15 | const par = n.parent; 16 | if ( 17 | allowedTrustedType && 18 | ts.isCallExpression(par) && 19 | par.arguments.length > 0 && 20 | isExpressionOfAllowedTrustedType(tc, par.arguments[0], allowedTrustedType) 21 | ) { 22 | return true; 23 | } 24 | 25 | return false; 26 | } 27 | 28 | function isPolyfill(n: ts.Node, matcher: AbsoluteMatcher) { 29 | if (matcher.filePath === 'GLOBAL') { 30 | const parent = n.parent; 31 | if ( 32 | parent && 33 | ts.isBinaryExpression(parent) && 34 | parent.left === n && 35 | parent.operatorToken.kind === ts.SyntaxKind.EqualsToken 36 | ) { 37 | return true; 38 | } 39 | } 40 | return false; 41 | } 42 | 43 | function checkIdentifierNode( 44 | tc: ts.TypeChecker, 45 | n: ts.Identifier, 46 | matcher: AbsoluteMatcher, 47 | allowedTrustedType: TrustedTypesConfig | undefined, 48 | ): ts.Node | undefined { 49 | const wholeExp = ts.isPropertyAccessExpression(n.parent) ? n.parent : n; 50 | if (isPolyfill(wholeExp, matcher)) return; 51 | if (!matcher.matches(n, tc)) return; 52 | if (isCalledWithAllowedTrustedType(tc, n, allowedTrustedType)) return; 53 | 54 | return wholeExp; 55 | } 56 | 57 | function checkElementAccessNode( 58 | tc: ts.TypeChecker, 59 | n: ts.ElementAccessExpression, 60 | matcher: AbsoluteMatcher, 61 | allowedTrustedType: TrustedTypesConfig | undefined, 62 | ): ts.Node | undefined { 63 | if (isPolyfill(n, matcher)) return; 64 | if (!matcher.matches(n.argumentExpression, tc)) return; 65 | if (isCalledWithAllowedTrustedType(tc, n, allowedTrustedType)) return; 66 | 67 | return n; 68 | } 69 | 70 | /** Engine for the BANNED_NAME pattern */ 71 | export class NameEngine extends PatternEngine { 72 | protected readonly banImport: boolean = false; 73 | 74 | register(checker: Checker) { 75 | for (const value of this.config.values) { 76 | const matcher = new AbsoluteMatcher(value, this.banImport); 77 | 78 | // `String.prototype.split` only returns emtpy array when both the 79 | // string and the splitter are empty. Here we should be able to safely 80 | // assert pop returns a non-null result. 81 | const bannedIdName = matcher.bannedName.split('.').pop()!; 82 | checker.onNamedIdentifier( 83 | bannedIdName, 84 | this.wrapCheckWithAllowlistingAndFixer((tc, n) => 85 | checkIdentifierNode(tc, n, matcher, this.config.allowedTrustedType), 86 | ), 87 | this.config.errorCode, 88 | ); 89 | 90 | // `checker.onNamedIdentifier` will not match the node if it is accessed 91 | // using property access expression (e.g. window['eval']). 92 | // We already found examples on how developers misuse this limitation 93 | // internally. 94 | // 95 | // This engine is inteded to ban global name identifiers, but even these 96 | // can be property accessed using `globalThis` or `window`. 97 | checker.onStringLiteralElementAccess( 98 | bannedIdName, 99 | this.wrapCheckWithAllowlistingAndFixer( 100 | (tc, n: ts.ElementAccessExpression) => 101 | checkElementAccessNode( 102 | tc, 103 | n, 104 | matcher, 105 | this.config.allowedTrustedType, 106 | ), 107 | ), 108 | this.config.errorCode, 109 | ); 110 | } 111 | } 112 | } 113 | 114 | /** Engine for the BANNED_IMPORTED_NAME pattern */ 115 | export class ImportedNameEngine extends NameEngine { 116 | protected override readonly banImport: boolean = true; 117 | } 118 | -------------------------------------------------------------------------------- /common/third_party/tsetse/util/pattern_engines/pattern_engine.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import {Allowlist} from '../../allowlist'; 4 | import {Checker} from '../../checker'; 5 | import {Fix, Fixer} from '../../util/fixer'; 6 | import {PatternEngineConfig} from '../../util/pattern_config'; 7 | import {shouldExamineNode} from '../ast_tools'; 8 | 9 | /** 10 | * A patternEngine is the logic that handles a specific PatternKind. 11 | */ 12 | export abstract class PatternEngine { 13 | private readonly allowlist: Allowlist; 14 | 15 | constructor( 16 | protected readonly ruleName: string, 17 | protected readonly config: PatternEngineConfig, 18 | protected readonly fixers?: Fixer[], 19 | ) { 20 | this.allowlist = new Allowlist(config.allowlistEntries); 21 | } 22 | 23 | /** 24 | * `register` will be called by the ConformanceRule to tell Tsetse the 25 | * PatternEngine will handle matching. Implementations should use 26 | * `checkAndFilterResults` as a wrapper for `check`. 27 | */ 28 | abstract register(checker: Checker): void; 29 | 30 | /** 31 | * A composer that wraps checking functions with code handling aspects of the 32 | * analysis that are not engine-specific, and which defers to the 33 | * subclass-specific logic afterwards. Subclasses should transform their 34 | * checking logic with this composer before registered on the checker. 35 | */ 36 | protected wrapCheckWithAllowlistingAndFixer( 37 | checkFunction: (tc: ts.TypeChecker, n: T) => ts.Node | undefined, 38 | ): (c: Checker, n: T) => void { 39 | return (c: Checker, n: T) => { 40 | const sf = n.getSourceFile(); 41 | if (!shouldExamineNode(n) || sf.isDeclarationFile) { 42 | return; 43 | } 44 | const matchedNode = checkFunction(c.typeChecker, n); 45 | if (matchedNode) { 46 | const fixes = this.fixers 47 | ?.map((fixer) => fixer.getFixForFlaggedNode(matchedNode)) 48 | ?.filter((fix): fix is Fix => fix !== undefined); 49 | c.addFailureAtNode( 50 | matchedNode, 51 | this.config.errorMessage, 52 | this.ruleName, 53 | this.allowlist, 54 | fixes, 55 | ); 56 | } 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /common/third_party/tsetse/util/pattern_engines/property_engine.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import {Checker} from '../../checker'; 4 | import {debugLog} from '../ast_tools'; 5 | import {PropertyMatcher} from '../property_matcher'; 6 | 7 | import {PatternEngine} from './pattern_engine'; 8 | 9 | /** Match an AST node with a property matcher. */ 10 | export function matchProperty( 11 | tc: ts.TypeChecker, 12 | n: ts.PropertyAccessExpression | ts.ElementAccessExpression, 13 | matcher: PropertyMatcher, 14 | ): ts.Node | undefined { 15 | debugLog(() => `inspecting ${n.getText().trim()}`); 16 | if (!matcher.typeMatches(tc.getTypeAtLocation(n.expression))) return; 17 | return n; 18 | } 19 | 20 | /** 21 | * Engine for the BANNED_PROPERTY pattern. It captures accesses to property 22 | * matching the spec regardless whether it's a read or write. 23 | */ 24 | export class PropertyEngine extends PatternEngine { 25 | protected registerWith(checker: Checker, matchNode: typeof matchProperty) { 26 | for (const value of this.config.values) { 27 | const matcher = PropertyMatcher.fromSpec(value); 28 | checker.onNamedPropertyAccess( 29 | matcher.bannedProperty, 30 | this.wrapCheckWithAllowlistingAndFixer((tc, n) => 31 | matchNode(tc, n, matcher), 32 | ), 33 | this.config.errorCode, 34 | ); 35 | 36 | checker.onStringLiteralElementAccess( 37 | matcher.bannedProperty, 38 | this.wrapCheckWithAllowlistingAndFixer((tc, n) => 39 | matchNode(tc, n, matcher), 40 | ), 41 | this.config.errorCode, 42 | ); 43 | } 44 | } 45 | 46 | register(checker: Checker) { 47 | this.registerWith(checker, matchProperty); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /common/third_party/tsetse/util/pattern_engines/property_non_constant_write_engine.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import {Checker} from '../../checker'; 4 | import {debugLog} from '../ast_tools'; 5 | import {isLiteral} from '../is_literal'; 6 | import {PropertyMatcher} from '../property_matcher'; 7 | 8 | import {matchPropertyWrite, PropertyWriteEngine} from './property_write_engine'; 9 | 10 | function matchPropertyNonConstantWrite( 11 | tc: ts.TypeChecker, 12 | n: ts.PropertyAccessExpression | ts.ElementAccessExpression, 13 | matcher: PropertyMatcher, 14 | ): ts.Node | undefined { 15 | debugLog(() => `inspecting ${n.getFullText().trim()}`); 16 | if (matchPropertyWrite(tc, n, matcher) === undefined) { 17 | return; 18 | } 19 | const rval = (n.parent as ts.BinaryExpression).right; 20 | if (isLiteral(tc, rval)) { 21 | debugLog( 22 | () => 23 | `Assigned value (${rval.getFullText()}) is a compile-time constant.`, 24 | ); 25 | return; 26 | } 27 | return n.parent; 28 | } 29 | 30 | /** 31 | * The engine for BANNED_PROPERTY_NON_CONSTANT_WRITE. 32 | */ 33 | export class PropertyNonConstantWriteEngine extends PropertyWriteEngine { 34 | override register(checker: Checker) { 35 | this.registerWith(checker, matchPropertyNonConstantWrite); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /common/third_party/tsetse/util/pattern_engines/property_write_engine.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import {Checker} from '../../checker'; 4 | import {debugLog} from '../ast_tools'; 5 | import {isExpressionOfAllowedTrustedType} from '../is_trusted_type'; 6 | import {PropertyMatcher} from '../property_matcher'; 7 | import {TrustedTypesConfig} from '../trusted_types_configuration'; 8 | 9 | import {matchProperty, PropertyEngine} from './property_engine'; 10 | 11 | /** Test if an AST node is a matched property write. */ 12 | export function matchPropertyWrite( 13 | tc: ts.TypeChecker, 14 | n: ts.PropertyAccessExpression | ts.ElementAccessExpression, 15 | matcher: PropertyMatcher, 16 | ): ts.BinaryExpression | undefined { 17 | debugLog(() => `inspecting ${n.parent.getText().trim()}`); 18 | 19 | if (matchProperty(tc, n, matcher) === undefined) return; 20 | 21 | const assignment = n.parent; 22 | 23 | if (!ts.isBinaryExpression(assignment)) return; 24 | // All properties we track are of the string type, so we only look at 25 | // `=` and `+=` operators. 26 | if ( 27 | assignment.operatorToken.kind !== ts.SyntaxKind.EqualsToken && 28 | assignment.operatorToken.kind !== ts.SyntaxKind.PlusEqualsToken 29 | ) { 30 | return; 31 | } 32 | if (assignment.left !== n) return; 33 | 34 | return assignment; 35 | } 36 | 37 | /** 38 | * Checks whether the access expression is part of an assignment (write) to the 39 | * matched property and the type of the right hand side value is not of the 40 | * an allowed type. 41 | * 42 | * Returns `undefined` if the property write is allowed and the assignment node 43 | * if the assignment should trigger violation. 44 | */ 45 | function allowTrustedExpressionOnMatchedProperty( 46 | allowedType: TrustedTypesConfig | undefined, 47 | tc: ts.TypeChecker, 48 | n: ts.PropertyAccessExpression | ts.ElementAccessExpression, 49 | matcher: PropertyMatcher, 50 | ): ts.BinaryExpression | undefined { 51 | const assignment = matchPropertyWrite(tc, n, matcher); 52 | if (!assignment) return; 53 | 54 | if ( 55 | allowedType && 56 | isExpressionOfAllowedTrustedType(tc, assignment.right, allowedType) 57 | ) { 58 | return; 59 | } 60 | 61 | return assignment; 62 | } 63 | 64 | /** 65 | * The engine for BANNED_PROPERTY_WRITE. Bans assignments to the restricted 66 | * properties unless the right hand side of the assignment is of an allowed 67 | * type. 68 | */ 69 | export class PropertyWriteEngine extends PropertyEngine { 70 | override register(checker: Checker) { 71 | this.registerWith(checker, (tc, n, m) => 72 | allowTrustedExpressionOnMatchedProperty( 73 | this.config.allowedTrustedType, 74 | tc, 75 | n, 76 | m, 77 | ), 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /common/third_party/tsetse/util/property_matcher.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | /** 4 | * This class matches a property access node, based on a property holder type 5 | * (through its name), i.e. a class, and a property name. 6 | * 7 | * The logic is voluntarily simple: if a matcher for `a.b` tests a `x.y` node, 8 | * it will return true if: 9 | * - `x` is of type `a` either directly (name-based) or through inheritance 10 | * (ditto), 11 | * - and, textually, `y` === `b`. 12 | * 13 | * Note that the logic is different from TS's type system: this matcher doesn't 14 | * have any knowledge of structural typing. 15 | */ 16 | export class PropertyMatcher { 17 | static fromSpec(spec: string): PropertyMatcher { 18 | if (spec.indexOf('.prototype.') === -1) { 19 | throw new Error(`BANNED_PROPERTY expects a .prototype in your query.`); 20 | } 21 | const requestParser = /^([\w\d_.-]+)\.prototype\.([\w\d_.-]+)$/; 22 | const matches = requestParser.exec(spec); 23 | if (!matches) { 24 | throw new Error('Cannot understand the BannedProperty spec' + spec); 25 | } 26 | const [bannedType, bannedProperty] = matches.slice(1); 27 | return new PropertyMatcher(bannedType, bannedProperty); 28 | } 29 | 30 | constructor( 31 | readonly bannedType: string, 32 | readonly bannedProperty: string, 33 | ) {} 34 | 35 | /** 36 | * @param n The PropertyAccessExpression we're looking at. 37 | */ 38 | matches(n: ts.PropertyAccessExpression, tc: ts.TypeChecker) { 39 | return ( 40 | n.name.text === this.bannedProperty && 41 | this.typeMatches(tc.getTypeAtLocation(n.expression)) 42 | ); 43 | } 44 | 45 | /** 46 | * Match types recursively in the lattice. This function over-approximates 47 | * the result by considering union types and intersection types as the same. 48 | */ 49 | typeMatches(inspectedType: ts.Type): boolean { 50 | // Skip checking mocked objects 51 | if (inspectedType.aliasSymbol?.escapedName === 'SpyObj') return false; 52 | 53 | // Exact type match 54 | if (inspectedType.getSymbol()?.getName() === this.bannedType) { 55 | return true; 56 | } 57 | 58 | // If the type is an intersection/union, check if any of the component 59 | // matches 60 | if (inspectedType.isUnionOrIntersection()) { 61 | return inspectedType.types.some((comp) => this.typeMatches(comp)); 62 | } 63 | 64 | const baseTypes = inspectedType.getBaseTypes() || []; 65 | return baseTypes.some((base) => this.typeMatches(base)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /common/third_party/tsetse/util/trusted_types_configuration.ts: -------------------------------------------------------------------------------- 1 | /** Names of all Trusted Types */ 2 | export type TrustedTypes = 'TrustedHTML' | 'TrustedScript' | 'TrustedScriptURL'; 3 | /** 4 | * Trusted Types configuration used to match Trusted values in the assignments 5 | * to sinks. 6 | */ 7 | export interface TrustedTypesConfig { 8 | allowAmbientTrustedTypesDeclaration: boolean; 9 | /** 10 | * A characteristic component of the absolute path of the definition file. 11 | */ 12 | modulePathMatcher: string; 13 | /** 14 | * The fully qualified name of the trusted type to allow. E.g. 15 | * "global.TrustedHTML". 16 | */ 17 | typeName: TrustedTypes; 18 | } 19 | 20 | /** 21 | * Create `TrustedTypesConfig` for the given Trusted Type. 22 | */ 23 | function createDefaultTrustedTypeConfig( 24 | type: TrustedTypes, 25 | ): TrustedTypesConfig { 26 | const config = { 27 | allowAmbientTrustedTypesDeclaration: true, 28 | // the module path may look like 29 | // "/home/username/.../node_modules/@types/trusted-types/" 30 | modulePathMatcher: '/node_modules/@types/trusted-types/', 31 | typeName: type, 32 | }; 33 | 34 | return config; 35 | } 36 | 37 | /** 38 | * Trusted Types configuration allowing usage of `TrustedHTML` for a given rule. 39 | */ 40 | export const TRUSTED_HTML = createDefaultTrustedTypeConfig('TrustedHTML'); 41 | 42 | /** 43 | * Trusted Types configuration allowing usage of `TrustedScript` for a given 44 | * rule. 45 | */ 46 | export const TRUSTED_SCRIPT = createDefaultTrustedTypeConfig('TrustedScript'); 47 | 48 | /** 49 | * Trusted Types configuration allowing usage of `TrustedScriptURL` for a given 50 | * rule. 51 | */ 52 | export const TRUSTED_SCRIPT_URL = 53 | createDefaultTrustedTypeConfig('TrustedScriptURL'); 54 | -------------------------------------------------------------------------------- /common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig-compile.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "types": ["node", "glob"], 6 | "outDir": "lib" 7 | }, 8 | "include": ["*.ts", "rules", "third_party"] 9 | } 10 | -------------------------------------------------------------------------------- /docs/supported-checks.md: -------------------------------------------------------------------------------- 1 | # Checks for detecting Trusted Types violations 2 | 3 | 4 | 5 | Rule Name | Checks Against 6 | ----------------------------------- | -------------- 7 | ban-base-href-assignments | Assignments to `.href` on 8 | ban-document-execcommand | Calls to `document.execCommand('insertHTML')` 9 | ban-document-write-calls | Calls to `document.write` 10 | ban-document-writeln-calls | Calls to `document.writeln` 11 | ban-domparser-parsefromstring | Calls to `DOMParser.parseFromString` 12 | ban-eval-calls | Calls to `eval` 13 | ban-element-innerhtml-assignments | Assignments to `.innerHTML` on any element 14 | ban-element-outerhtml-assignments | Assignments to `.outerHTML` on any element 15 | ban-element-insertadjacenthtml | Calls to `.insertAdjacentHTML` on any element 16 | ban-element-setattribute | Calls to `.setAttribute` on any element with dangerous attribute names 17 | ban-iframe-srcdoc-assignments | Assignments to `.srcdoc` on