@qes-test/eslint-config
Version:
ESLint configuration for QES projects
284 lines (281 loc) • 10.9 kB
JavaScript
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 };