UNPKG

@unito/integration-sdk

Version:

Integration SDK

104 lines (103 loc) 4.06 kB
import { isInsideCatchClause, isHttpErrorsCallee } from '../utils.js'; const SCOPE_FILE_RE = /\/src\/(handlers|routes)\//; const RATE_LIMITER_RE = /[Rr]ate[Ll]imit/; const ERROR_IDENTIFIER_RE = /^(err|error|e|originalError|cause)$/i; function getPropertyName(property) { if (!property) { return null; } if (property.type === 'Identifier') { return property.name; } if (property.type === 'Literal' && typeof property.value === 'string') { return property.value; } return null; } function isErrorMessageAccess(memberExpression) { if (memberExpression.type !== 'MemberExpression') { return false; } if (getPropertyName(memberExpression.property) !== 'message') { return false; } if (memberExpression.object.type !== 'Identifier') { return false; } return ERROR_IDENTIFIER_RE.test(memberExpression.object.name); } function containsErrorMessageAccess(node) { if (!node) { return false; } if (node.type === 'MemberExpression') { if (isErrorMessageAccess(node)) { return true; } return containsErrorMessageAccess(node.object) || containsErrorMessageAccess(node.property); } if (node.type === 'TemplateLiteral') { return node.expressions.some(e => containsErrorMessageAccess(e)); } if (node.type === 'ConditionalExpression') { return (containsErrorMessageAccess(node.test) || containsErrorMessageAccess(node.consequent) || containsErrorMessageAccess(node.alternate)); } if (node.type === 'BinaryExpression' || node.type === 'LogicalExpression') { return containsErrorMessageAccess(node.left) || containsErrorMessageAccess(node.right); } if (node.type === 'CallExpression' || node.type === 'NewExpression') { return node.arguments.some(arg => containsErrorMessageAccess(arg)); } // TS-specific node types (cast / non-null assertion). `estree` doesn't declare // them but @typescript-eslint/parser emits them; treat them as transparent // wrappers and recurse on .expression. const ts = node; if (ts.type === 'TSAsExpression' || ts.type === 'TSNonNullExpression') { return containsErrorMessageAccess(ts.expression); } return false; } const rule = { meta: { type: 'problem', docs: { description: 'Disallow rethrowing the provider error message into the HTTP response (`HttpErrors.X(err.message)`); strip the provider message and throw a stable summary across the boundary', recommended: true, }, schema: [], messages: { errorMessageRethrow: 'Do not propagate `error.message` (or `err.message`/`e.message`) into a `HttpErrors.*` throw. Provider error strings can carry tokens, PII, or internal field names — rethrowing them across the HTTP boundary turns the connector into an exfiltration path. Log the original message via the structured logger (key-based redaction) and throw a stable, provider-agnostic summary instead.', }, }, create(context) { const filename = context.filename ?? ''; if (!SCOPE_FILE_RE.test(filename)) { return {}; } if (RATE_LIMITER_RE.test(filename)) { return {}; } return { ThrowStatement(node) { const argument = node.argument; if (!argument || argument.type !== 'NewExpression') { return; } if (!isHttpErrorsCallee(argument.callee)) { return; } if (!isInsideCatchClause(node)) { return; } const propagates = argument.arguments.some(arg => containsErrorMessageAccess(arg)); if (!propagates) { return; } context.report({ node, messageId: 'errorMessageRethrow' }); }, }; }, }; export default rule;