mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
* chore: create eslint rule to catch insecure page script injection * chore: ignore existing lints * review: tighten rule scope * review: add tests
116 lines
3.5 KiB
JavaScript
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",
|
|
});
|
|
}
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|