1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 13:53:34 +00:00
Files
browser/libs/eslint/platform/no-page-script-url-leakage.mjs
Addison Beck ba93526965 chore: create eslint rule to catch insecure page script injection (#17437)
* chore: create eslint rule to catch insecure page script injection

* chore: ignore existing lints

* review: tighten rule scope

* review: add tests
2025-11-20 19:45:49 -05:00

116 lines
3.5 KiB
JavaScript

/**
* @fileoverview ESLint rule to prevent page script URL leakage vulnerabilities
* @description This rule detects the specific security vulnerability where DOM script elements
* receive extension URLs through chrome.runtime.getURL() or browser.runtime.getURL() calls.
* This pattern exposes predictable extension URLs to web pages, enabling fingerprinting attacks.
*/
export const errorMessage =
"Script injection with extension URL exposes asset urls. Use secure page script registration instead.";
/**
* Checks if a node is a call to chrome.runtime.getURL() or browser.runtime.getURL()
* @param {Object} node - The AST node to check
* @returns {boolean} True if the node is an extension URL call
*/
function isExtensionURLCall(node) {
return (
node &&
node.type === "CallExpression" &&
node.callee &&
node.callee.type === "MemberExpression" &&
node.callee.object &&
node.callee.object.type === "MemberExpression" &&
node.callee.object.object &&
["chrome", "browser"].includes(node.callee.object.object.name) &&
node.callee.object.property &&
node.callee.object.property.name === "runtime" &&
node.callee.property &&
node.callee.property.name === "getURL"
);
}
/**
* Checks if a node is a call to createElement("script")
* @param {Object} node - The AST node to check
* @returns {boolean} True if the node creates a script element
*/
function isScriptCreation(node) {
return (
node &&
node.type === "CallExpression" &&
node.callee &&
node.callee.type === "MemberExpression" &&
node.callee.property &&
node.callee.property.name === "createElement" &&
node.arguments &&
node.arguments.length === 1 &&
node.arguments[0] &&
node.arguments[0].type === "Literal" &&
node.arguments[0].value === "script"
);
}
export default {
meta: {
type: "problem",
docs: {
description: "Prevent page script URL leakage through extension runtime.getURL calls",
category: "Security",
recommended: true,
},
schema: [],
messages: {
pageScriptUrlLeakage: errorMessage,
},
},
create(context) {
const scriptVariables = new Set();
return {
// Track createElement("script") calls to identify script variables
VariableDeclarator(node) {
if (node.init && isScriptCreation(node.init) && node.id && node.id.name) {
scriptVariables.add(node.id.name);
}
},
// Track assignments where script elements are created
AssignmentExpression(node) {
// Track script element creation: variable = document.createElement("script")
if (
node.operator === "=" &&
node.left &&
node.left.type === "Identifier" &&
isScriptCreation(node.right)
) {
scriptVariables.add(node.left.name);
}
// Check for script.src = extension URL pattern
if (
node.operator === "=" &&
node.left &&
node.left.type === "MemberExpression" &&
node.left.property &&
node.left.property.name === "src" &&
isExtensionURLCall(node.right)
) {
// Only flag if this is a script element assignment
if (
node.left.object &&
node.left.object.type === "Identifier" &&
scriptVariables.has(node.left.object.name)
) {
context.report({
node: node.right,
messageId: "pageScriptUrlLeakage",
});
}
}
},
};
},
};