UNPKG

@cosmstack/blackshield

Version:

A developer-first security toolkit for React/Next.js applications

171 lines (168 loc) 5.05 kB
// src/rules/no-public-sensitive-env.ts import { ESLintUtils } from "@typescript-eslint/utils"; var createRule = ESLintUtils.RuleCreator( (name) => `https://github.com/cosmstack/blackshield/blob/main/docs/rules/${name}.md` ); var SENSITIVE_PATTERNS = [ /SECRET/i, /KEY/i, /TOKEN/i, /PASSWORD/i, /PRIVATE/i, /API_SECRET/i, /DATABASE/i, /DB_/i, /MONGO/i, /REDIS/i, /JWT/i, /AUTH/i, /STRIPE_SECRET/i, /PAYPAL/i, /WEBHOOK/i, /ENCRYPTION/i, /HASH/i, /SALT/i ]; var noPublicSensitiveEnv = createRule({ name: "no-public-sensitive-env", meta: { type: "problem", docs: { description: "Prevent access to sensitive environment variables exposed through NEXT_PUBLIC_" }, fixable: "code", schema: [ { type: "object", properties: { allowedVars: { type: "array", items: { type: "string" } }, customPatterns: { type: "array", items: { type: "string" } } }, additionalProperties: false } ], messages: { sensitivePublicEnv: 'Accessing potentially sensitive environment variable "{{varName}}" that is exposed to client', suggestServerSide: "Move this variable to server-side code or remove NEXT_PUBLIC_ prefix", suggestAllowList: "If this variable is safe to expose, add it to allowedVars in ESLint config" } }, defaultOptions: [{ allowedVars: [], customPatterns: [] }], create(context, [options]) { const allowedVars = options.allowedVars || []; const customPatterns = (options.customPatterns || []).map((p) => new RegExp(p, "i")); const allPatterns = [...SENSITIVE_PATTERNS, ...customPatterns]; return { MemberExpression(node) { if (node.object.type === "MemberExpression" && node.object.object.type === "Identifier" && node.object.object.name === "process" && node.object.property.type === "Identifier" && node.object.property.name === "env" && node.property.type === "Identifier") { const varName = node.property.name; if (varName.startsWith("NEXT_PUBLIC_")) { if (allowedVars.includes(varName)) return; const isSensitive = allPatterns.some((pattern) => pattern.test(varName)); if (isSensitive) { context.report({ node, messageId: "sensitivePublicEnv", data: { varName }, suggest: [ { messageId: "suggestServerSide", fix(fixer) { return fixer.replaceText(node.property, varName.replace("NEXT_PUBLIC_", "")); } }, { messageId: "suggestAllowList", fix() { return null; } } ] }); } } } } }; } }); // src/rules/no-unsafe-html.ts import { ESLintUtils as ESLintUtils2 } from "@typescript-eslint/utils"; var createRule2 = ESLintUtils2.RuleCreator( (name) => `https://github.com/cosmstack/blackshield/blob/main/docs/rules/${name}.md` ); var noUnsafeHtml = createRule2({ name: "no-unsafe-html", meta: { type: "problem", docs: { description: "Prevent unsafe HTML injection vulnerabilities" }, fixable: "code", schema: [], messages: { unsafeHtml: "Using dangerouslySetInnerHTML without sanitization can lead to XSS vulnerabilities", suggestSafeHtml: "Use SafeHTML component from @cosmstack/blackshield instead", unsafeInnerHtml: "Direct innerHTML assignment can lead to XSS vulnerabilities" } }, defaultOptions: [], create(context) { return { JSXAttribute(node) { if (node.name.type === "JSXIdentifier" && node.name.name === "dangerouslySetInnerHTML") { context.report({ node, messageId: "unsafeHtml", suggest: [ { messageId: "suggestSafeHtml", fix(fixer) { return fixer.replaceText(node.name, "html"); } } ] }); } }, AssignmentExpression(node) { if (node.left.type === "MemberExpression" && node.left.property.type === "Identifier" && node.left.property.name === "innerHTML") { context.report({ node, messageId: "unsafeInnerHtml" }); } } }; } }); // src/rules/index.ts var rules = { "no-unsafe-html": noUnsafeHtml, "no-public-sensitive-env": noPublicSensitiveEnv }; var configs = { recommended: { plugins: ["@cosmstack/blackshield/eslint-plugin"], rules: { "@cosmstack/blackshield/no-unsafe-html": "error", "@cosmstack/blackshield/no-public-sensitive-env": "error" } } }; var plugin = { rules, configs }; var index_default = plugin; export { configs, index_default as default, rules };