UNPKG

@qes-test/eslint-config

Version:

ESLint configuration for QES projects

284 lines (281 loc) 10.9 kB
const rule = { meta: { type: "problem", docs: { description: "\u5F3A\u5236\u5B89\u5168\u4F7F\u7528 postMessage \u548C message \u4E8B\u4EF6\u76D1\u542C\uFF0C\u5305\u62EC\u4E25\u683C\u7684 origin \u9A8C\u8BC1", category: "Security", recommended: true, url: "https://your-docs-url.com" }, messages: { missingOriginCheck: "\u5FC5\u987B\u68C0\u67E5\u6D88\u606F\u6765\u6E90 (event.origin) \u4EE5\u786E\u4FDD\u5B89\u5168\u6027", insufficientOriginCheck: "\u5FC5\u987B\u5BF9 origin \u8FDB\u884C\u4E25\u683C\u9A8C\u8BC1\uFF08\u5982\u767D\u540D\u5355\u68C0\u67E5\u3001\u6B63\u5219\u5339\u914D\u6216\u7CBE\u786E\u6BD4\u8F83\uFF09\uFF0C\u4E0D\u80FD\u4EC5\u68C0\u67E5\u662F\u5426\u5B58\u5728", insecureWildcardOrigin: "\u7981\u6B62\u4F7F\u7528\u901A\u914D\u7B26 ('*') \u6216\u7A7A\u5B57\u7B26\u4E32 ('') \u4F5C\u4E3A postMessage \u7684\u76EE\u6807 origin", missingEventParameter: "\u6D88\u606F\u4E8B\u4EF6\u56DE\u8C03\u51FD\u6570\u5FC5\u987B\u5305\u542B\u4E8B\u4EF6\u53C2\u6570", wildcardVariableUsage: "\u68C0\u6D4B\u5230\u53D8\u91CF {{variableName}} \u5305\u542B\u901A\u914D\u7B26 (*) \u6216\u7A7A\u5B57\u7B26\u4E32\uFF0C\u4E0D\u80FD\u7528\u4F5C postMessage \u7684 origin", missingOriginParameter: "postMessage \u5FC5\u987B\u660E\u786E\u6307\u5B9A\u5B89\u5168\u7684 origin \u53C2\u6570", unsafeOptionalChaining: "\u907F\u514D\u5728 postMessage \u8C03\u7528\u4E2D\u4F7F\u7528\u53EF\u9009\u7684\u94FE\u5F0F\u8C03\u7528\uFF0C\u8FD9\u53EF\u80FD\u5BFC\u81F4 origin \u9A8C\u8BC1\u88AB\u7ED5\u8FC7" }, schema: [] }, create(context) { const eventParamNames = /* @__PURE__ */ new Map(); const wildcardVariables = /* @__PURE__ */ new Set(); const destructuredOriginVars = /* @__PURE__ */ new Map(); return { // 只记录通配符或空字符串变量,不立即报错 "VariableDeclarator[init.value='*'], VariableDeclarator[init.value='']": function(node) { if (node.id.type === "Identifier") { wildcardVariables.add(node.id.name); } }, "AssignmentExpression[right.value='*'], AssignmentExpression[right.value='']": function(node) { if (node.left.type === "Identifier") { wildcardVariables.add(node.left.name); } }, // 检查消息事件监听器 "CallExpression[callee.property.name='addEventListener']": function(node) { if (node.arguments.length >= 2 && node.arguments[0].type === "Literal" && node.arguments[0].value === "message" && (node.arguments[1].type === "ArrowFunctionExpression" || node.arguments[1].type === "FunctionExpression")) { const callback = node.arguments[1]; if (callback.params.length === 0) { context.report({ node: callback, messageId: "missingEventParameter" }); return; } const eventParamName = callback.params[0].name; eventParamNames.set(callback.body, eventParamName); if (callback.body.type === "BlockStatement") { callback.body.body.forEach((statement) => { if (statement.type === "VariableDeclaration" && statement.declarations.length > 0) { const declaration = statement.declarations[0]; if (declaration.id.type === "ObjectPattern" && declaration.init?.type === "Identifier" && declaration.init.name === eventParamName) { const originProperty = declaration.id.properties.find( (prop) => (prop.key?.name === "origin" || prop.value?.name === "origin") && prop.type === "Property" ); if (originProperty) { const originVarName = originProperty.value?.name || (originProperty.key?.name === "origin" && declaration.id.type === "ObjectPattern" ? originProperty.value.name : null); if (originVarName) { destructuredOriginVars.set(callback.body, originVarName); } } } } }); } const { hasOriginCheck, isSufficient } = checkOriginValidation( callback.body, eventParamName, destructuredOriginVars.get(callback.body) ); if (!hasOriginCheck) { context.report({ node: callback, messageId: "missingOriginCheck" }); } else if (!isSufficient) { context.report({ node: callback, messageId: "insufficientOriginCheck" }); } } }, // 检查 postMessage 调用 "CallExpression[callee.property.name='postMessage']": function(node) { if (node.arguments.length < 2) { context.report({ node, messageId: "missingOriginParameter" }); return; } const originArg = node.arguments[1]; if (originArg.type === "Literal" && (originArg.value === "*" || originArg.value === "")) { context.report({ node: originArg, messageId: "insecureWildcardOrigin" }); return; } if (originArg.type === "Identifier" && wildcardVariables.has(originArg.name)) { context.report({ node: originArg, messageId: "wildcardVariableUsage", data: { variableName: originArg.name } }); return; } if (originArg.type === "MemberExpression" && (originArg.object.type === "Identifier" && wildcardVariables.has(originArg.object.name) || originArg.object.type === "Literal" && (originArg.object.value === "*" || originArg.object.value === ""))) { context.report({ node: originArg, messageId: "wildcardVariableUsage", data: { variableName: originArg.object.type === "Identifier" ? originArg.object.name : "literal" } }); } } }; } }; function checkOriginValidation(node, eventParamName, destructuredOriginName) { if (!node) return { hasOriginCheck: false, isSufficient: false }; let hasCheck = false; let isSufficient = false; const checkNode = (n) => { const result = checkCondition(n, eventParamName, destructuredOriginName); hasCheck = hasCheck || result.hasCheck; isSufficient = isSufficient || result.isSufficient; }; if (node.type === "BlockStatement") { node.body.forEach(checkNode); } else { checkNode(node); } return { hasOriginCheck: hasCheck, isSufficient }; } function checkCondition(node, eventParamName, destructuredOriginName) { if (!node) return { hasCheck: false, isSufficient: false }; switch (node.type) { case "BinaryExpression": if (node.operator === "===" || node.operator === "!==") { const left2 = checkOriginAccess( node.left, eventParamName, destructuredOriginName ); const right2 = checkOriginAccess( node.right, eventParamName, destructuredOriginName ); if (left2.isOrigin && (right2.isString || right2.isRegex || right2.isVariable) || right2.isOrigin && (left2.isString || left2.isRegex || left2.isVariable)) { return { hasCheck: true, isSufficient: true }; } } return { hasCheck: false, isSufficient: false }; case "CallExpression": if (node.callee.type === "MemberExpression") { const methodName = node.callee.property.name; const validMethods = [ "test", "includes", "indexOf", "startsWith", "endsWith" ]; if (validMethods.includes(methodName)) { const obj = checkOriginAccess( node.callee.object, eventParamName, destructuredOriginName ); const arg = node.arguments[0] && checkOriginAccess( node.arguments[0], eventParamName, destructuredOriginName ); if (obj.isOrigin && (arg.isString || arg.isRegex || arg.isVariable) || (obj.isString || obj.isRegex || obj.isVariable) && arg.isOrigin) { return { hasCheck: true, isSufficient: true }; } } } return { hasCheck: false, isSufficient: false }; case "IfStatement": return checkCondition(node.test, eventParamName, destructuredOriginName); case "LogicalExpression": const left = checkCondition( node.left, eventParamName, destructuredOriginName ); const right = checkCondition( node.right, eventParamName, destructuredOriginName ); return { hasCheck: left.hasCheck || right.hasCheck, isSufficient: left.isSufficient || right.isSufficient }; case "MemberExpression": const access = checkOriginAccess( node, eventParamName, destructuredOriginName ); return { hasCheck: access.isOrigin, isSufficient: false }; case "UnaryExpression": if (node.operator === "!") { return checkCondition( node.argument, eventParamName, destructuredOriginName ); } return { hasCheck: false, isSufficient: false }; default: return { hasCheck: false, isSufficient: false }; } } function checkOriginAccess(node, eventParamName, destructuredOriginName) { if (!node) return { isOrigin: false, isString: false, isRegex: false, isVariable: false }; if (destructuredOriginName && node.type === "Identifier" && node.name === destructuredOriginName) { return { isOrigin: true, isString: false, isRegex: false, isVariable: false }; } if (node.type === "MemberExpression" && (node.property.name === "origin" || node.property.value === "origin") && node.object.type === "Identifier" && node.object.name === eventParamName) { return { isOrigin: true, isString: false, isRegex: false, isVariable: false }; } if (node.type === "Literal" && typeof node.value === "string") { return { isOrigin: false, isString: true, isRegex: false, isVariable: false }; } if (node.type === "Literal" && node.regex) { return { isOrigin: false, isString: false, isRegex: true, isVariable: false }; } if (node.type === "Identifier") { return { isOrigin: false, isString: false, isRegex: false, isVariable: true }; } if (node.type === "TemplateLiteral") { return { isOrigin: false, isString: true, isRegex: false, isVariable: false }; } return { isOrigin: false, isString: false, isRegex: false, isVariable: false }; } const index = { rules: { "check-message-origin": rule } }; export { index as default };