@shipsafe/cli 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1122,6 +1122,9 @@ const RULES = [
1122
1122
  detect: (line) => {
1123
1123
  if (/process\s*\.\s*env\b/.test(line) || /os\s*\.\s*(?:environ|getenv)\b/.test(line))
1124
1124
  return false;
1125
+ // Skip localhost/dev connection strings — not production credentials
1126
+ if (/@(?:localhost|127\.0\.0\.1|0\.0\.0\.0|::1)(?:[:/]|$)/i.test(line))
1127
+ return false;
1125
1128
  // Match hardcoded connection strings: mongodb://, postgres://, mysql://, redis:// with credentials
1126
1129
  return /['"](?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|redis|amqp|mssql):\/\/[^'"]*:[^'"]*@[^'"]+['"]/.test(line);
1127
1130
  },
@@ -1763,6 +1766,479 @@ const RULES = [
1763
1766
  return /\bos\s*\.\s*popen\s*\(/.test(line);
1764
1767
  },
1765
1768
  },
1769
+ // ════════════════════════════════════════════
1770
+ // Next.js / React Specific
1771
+ // ════════════════════════════════════════════
1772
+ {
1773
+ id: 'NEXT_SENSITIVE_PROPS',
1774
+ category: 'Sensitive Data Exposure',
1775
+ description: 'getServerSideProps or getStaticProps returns sensitive data (password, token, secret) in props — this data is serialized to the client.',
1776
+ severity: 'high',
1777
+ fix_suggestion: 'Never return sensitive fields (passwords, tokens, secrets) in page props. Strip sensitive data before returning props.',
1778
+ auto_fixable: false,
1779
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
1780
+ skipCommentsAndStrings: false,
1781
+ skipTestFiles: true,
1782
+ detect: (line, ctx) => {
1783
+ // Check if we're in a getServerSideProps/getStaticProps function
1784
+ const lineIdx = ctx.lineNumber - 1;
1785
+ const window = ctx.allLines
1786
+ .slice(Math.max(0, lineIdx - 10), Math.min(ctx.allLines.length, lineIdx + 3))
1787
+ .join(' ');
1788
+ if (!/\b(?:getServerSideProps|getStaticProps)\b/.test(window))
1789
+ return false;
1790
+ // Check if props contain sensitive field names
1791
+ if (!/\bprops\s*:/.test(line) && !/\bprops\s*:/.test(window))
1792
+ return false;
1793
+ const propsWindow = ctx.allLines
1794
+ .slice(Math.max(0, lineIdx - 2), Math.min(ctx.allLines.length, lineIdx + 5))
1795
+ .join(' ');
1796
+ return /\b(?:password|secret|token|apiKey|api_key|privateKey|private_key|ssn|creditCard|credit_card)\s*[:=]/i.test(propsWindow) &&
1797
+ /\bprops\s*:/.test(propsWindow);
1798
+ },
1799
+ },
1800
+ {
1801
+ id: 'NEXT_API_NO_AUTH',
1802
+ category: 'Authentication Issues',
1803
+ description: 'Next.js API route handler (export default function handler) without apparent authentication check — endpoint may be publicly accessible.',
1804
+ severity: 'medium',
1805
+ fix_suggestion: 'Add authentication checks (getSession, getServerSession, auth middleware) at the beginning of API route handlers.',
1806
+ auto_fixable: false,
1807
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
1808
+ skipCommentsAndStrings: true,
1809
+ skipTestFiles: true,
1810
+ detect: (line, ctx) => {
1811
+ // Match export default function handler pattern
1812
+ if (!/\bexport\s+default\s+(?:async\s+)?function\s+handler\b/.test(line))
1813
+ return false;
1814
+ // Check entire file for auth checks
1815
+ return !/\b(?:auth|getSession|getServerSession|requireAuth|isAuthenticated|protect|guard|verify|middleware|getToken|withAuth|checkAuth|session)\b/i.test(ctx.fileContent);
1816
+ },
1817
+ },
1818
+ {
1819
+ id: 'NEXT_REVALIDATE_USER_INPUT',
1820
+ category: 'Cache Poisoning',
1821
+ description: 'revalidateTag() or revalidatePath() called with user-supplied input — may allow cache manipulation attacks.',
1822
+ severity: 'medium',
1823
+ fix_suggestion: 'Validate revalidation tags/paths against an allowlist. Never pass raw user input to revalidateTag or revalidatePath.',
1824
+ auto_fixable: false,
1825
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
1826
+ skipCommentsAndStrings: true,
1827
+ skipTestFiles: true,
1828
+ detect: (line, ctx) => {
1829
+ if (!/\b(?:revalidateTag|revalidatePath)\s*\(\s*[a-zA-Z_$]/.test(line))
1830
+ return false;
1831
+ // Exclude calls with string literals
1832
+ if (/\b(?:revalidateTag|revalidatePath)\s*\(\s*['"]/.test(line))
1833
+ return false;
1834
+ // Check if the variable likely comes from user input
1835
+ const lineIdx = ctx.lineNumber - 1;
1836
+ const window = ctx.allLines
1837
+ .slice(Math.max(0, lineIdx - 10), lineIdx + 1)
1838
+ .join(' ');
1839
+ return /\breq\b|\.json\(\)|\.body\b|\.query\b|\.params\b|\.searchParams\b/.test(window);
1840
+ },
1841
+ },
1842
+ // ════════════════════════════════════════════
1843
+ // GraphQL Security
1844
+ // ════════════════════════════════════════════
1845
+ {
1846
+ id: 'GRAPHQL_INTROSPECTION_ENABLED',
1847
+ category: 'Insecure Configuration',
1848
+ description: 'GraphQL introspection is explicitly enabled — in production this exposes the full API schema to attackers.',
1849
+ severity: 'medium',
1850
+ fix_suggestion: 'Disable introspection in production: new ApolloServer({ introspection: process.env.NODE_ENV !== "production" }).',
1851
+ auto_fixable: true,
1852
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
1853
+ skipCommentsAndStrings: false,
1854
+ skipTestFiles: true,
1855
+ detect: (line) => {
1856
+ return /\bintrospection\s*:\s*true\b/.test(line);
1857
+ },
1858
+ },
1859
+ {
1860
+ id: 'GRAPHQL_NO_DEPTH_LIMIT',
1861
+ category: 'Insecure Configuration',
1862
+ description: 'GraphQL server created without query depth limiting — vulnerable to resource exhaustion via deeply nested queries.',
1863
+ severity: 'medium',
1864
+ fix_suggestion: 'Add a query depth limit plugin: new ApolloServer({ plugins: [depthLimit(10)] }). Install graphql-depth-limit or @graphql-tools/utils.',
1865
+ auto_fixable: false,
1866
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
1867
+ skipCommentsAndStrings: false,
1868
+ skipTestFiles: true,
1869
+ detect: (line, ctx) => {
1870
+ // Only trigger on ApolloServer/YogaServer/GraphQLServer creation
1871
+ if (!/\bnew\s+(?:ApolloServer|YogaServer|GraphQLServer)\s*\(/.test(line))
1872
+ return false;
1873
+ // Check the whole file for depth limit usage
1874
+ return !/\b(?:depthLimit|depth[_-]?limit|maxDepth|max[_-]?depth|queryDepth|query[_-]?depth)\b/i.test(ctx.fileContent);
1875
+ },
1876
+ },
1877
+ {
1878
+ id: 'GRAPHQL_MUTATION_NO_AUTH',
1879
+ category: 'Authentication Issues',
1880
+ description: 'GraphQL mutation resolver accesses the database without an apparent authentication check — mutations should verify the caller\'s identity.',
1881
+ severity: 'high',
1882
+ fix_suggestion: 'Add authentication checks in mutation resolvers. Verify the user is authenticated via context (e.g., context.user, context.auth) before performing database operations.',
1883
+ auto_fixable: false,
1884
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
1885
+ skipCommentsAndStrings: true,
1886
+ skipTestFiles: true,
1887
+ detect: (line, ctx) => {
1888
+ // Look for Mutation resolver definitions
1889
+ if (!/\bMutation\s*:\s*\{/.test(line))
1890
+ return false;
1891
+ // Check a window for auth checks in the mutation block
1892
+ const lineIdx = ctx.lineNumber - 1;
1893
+ const window = ctx.allLines
1894
+ .slice(lineIdx, Math.min(ctx.allLines.length, lineIdx + 20))
1895
+ .join(' ');
1896
+ // If there's DB access without auth
1897
+ const hasDbAccess = /\b(?:db|prisma|knex|sequelize|mongoose|pool|client)\s*\.\s*(?:query|find|create|update|delete|insert|remove|exec|run)\b/i.test(window);
1898
+ const hasAuth = /\b(?:auth|context\s*\.\s*(?:user|auth|session|token)|requireAuth|isAuthenticated|authorize)\b/i.test(window);
1899
+ return hasDbAccess && !hasAuth;
1900
+ },
1901
+ },
1902
+ // ════════════════════════════════════════════
1903
+ // WebSocket Security
1904
+ // ════════════════════════════════════════════
1905
+ {
1906
+ id: 'WEBSOCKET_NO_AUTH',
1907
+ category: 'Authentication Issues',
1908
+ description: 'WebSocket server created without origin validation or authentication (verifyClient) — any origin can connect.',
1909
+ severity: 'medium',
1910
+ fix_suggestion: 'Add a verifyClient callback to validate the origin and/or authenticate connections: new WebSocketServer({ verifyClient: (info) => validateOrigin(info.origin) }).',
1911
+ auto_fixable: false,
1912
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
1913
+ skipCommentsAndStrings: true,
1914
+ skipTestFiles: true,
1915
+ detect: (line, ctx) => {
1916
+ if (!/\bnew\s+WebSocketServer\s*\(/.test(line))
1917
+ return false;
1918
+ // Check a window for verifyClient or authentication
1919
+ const lineIdx = ctx.lineNumber - 1;
1920
+ const window = ctx.allLines
1921
+ .slice(lineIdx, Math.min(ctx.allLines.length, lineIdx + 5))
1922
+ .join(' ');
1923
+ return !/\b(?:verifyClient|authenticate|auth|handleAuth)\b/i.test(window);
1924
+ },
1925
+ },
1926
+ {
1927
+ id: 'WEBSOCKET_BROADCAST_UNSANITIZED',
1928
+ category: 'Cross-Site Scripting (XSS)',
1929
+ description: 'WebSocket message broadcast to all clients without sanitization — enables XSS or injection via malicious messages.',
1930
+ severity: 'medium',
1931
+ fix_suggestion: 'Sanitize and validate WebSocket messages before broadcasting. Filter or escape HTML/script content, and validate message format.',
1932
+ auto_fixable: false,
1933
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
1934
+ skipCommentsAndStrings: true,
1935
+ skipTestFiles: true,
1936
+ detect: (line, ctx) => {
1937
+ // Detect patterns like clients.forEach(client => client.send(msg))
1938
+ if (!/\bclients\b.*\b(?:forEach|for)\b.*\bsend\s*\(/.test(line))
1939
+ return false;
1940
+ // Check if the message is sanitized before broadcast
1941
+ const lineIdx = ctx.lineNumber - 1;
1942
+ const window = ctx.allLines
1943
+ .slice(Math.max(0, lineIdx - 5), lineIdx + 1)
1944
+ .join(' ');
1945
+ return !/\b(?:sanitize|escape|validate|filter|encode|DOMPurify|xss)\b/i.test(window);
1946
+ },
1947
+ },
1948
+ // ════════════════════════════════════════════
1949
+ // JWT Edge Cases
1950
+ // ════════════════════════════════════════════
1951
+ {
1952
+ id: 'JWT_ALG_NONE',
1953
+ category: 'Authentication Issues',
1954
+ description: 'JWT verification accepts the "none" algorithm — attackers can forge tokens by specifying alg: "none" with no signature.',
1955
+ severity: 'critical',
1956
+ fix_suggestion: 'Never allow the "none" algorithm. Explicitly specify only the algorithms you use: jwt.verify(token, key, { algorithms: ["HS256"] }).',
1957
+ auto_fixable: true,
1958
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
1959
+ skipCommentsAndStrings: false,
1960
+ skipTestFiles: true,
1961
+ detect: (line) => {
1962
+ // Match algorithms array containing 'none'
1963
+ return /algorithms\s*:\s*\[.*['"]none['"]/.test(line);
1964
+ },
1965
+ },
1966
+ {
1967
+ id: 'JWT_DECODE_WITHOUT_VERIFY',
1968
+ category: 'Authentication Issues',
1969
+ description: 'jwt.decode() returns an unverified payload — using it for authorization decisions allows token forgery.',
1970
+ severity: 'high',
1971
+ fix_suggestion: 'Use jwt.verify() instead of jwt.decode() for any authorization or authentication logic. jwt.decode() does NOT validate the signature.',
1972
+ auto_fixable: false,
1973
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
1974
+ skipCommentsAndStrings: true,
1975
+ skipTestFiles: true,
1976
+ detect: (line, ctx) => {
1977
+ if (!/\bjwt\s*\.\s*decode\s*\(/.test(line))
1978
+ return false;
1979
+ // Check if the decoded value is used for auth decisions in nearby lines
1980
+ const lineIdx = ctx.lineNumber - 1;
1981
+ const window = ctx.allLines
1982
+ .slice(lineIdx, Math.min(ctx.allLines.length, lineIdx + 5))
1983
+ .join(' ');
1984
+ return /\b(?:role|admin|isAdmin|is_admin|permission|authorized|auth|user)\b/i.test(window) ||
1985
+ /\bif\s*\(/.test(window);
1986
+ },
1987
+ },
1988
+ {
1989
+ id: 'JWT_BEARER_PREFIX',
1990
+ category: 'Authentication Issues',
1991
+ description: 'Authorization header value used directly without stripping the "Bearer " prefix — jwt.verify() will fail or behave unexpectedly.',
1992
+ severity: 'medium',
1993
+ fix_suggestion: 'Strip the "Bearer " prefix before verification: const token = req.headers.authorization?.replace("Bearer ", ""); then jwt.verify(token, ...).',
1994
+ auto_fixable: true,
1995
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
1996
+ skipCommentsAndStrings: true,
1997
+ skipTestFiles: true,
1998
+ detect: (line, ctx) => {
1999
+ // Detect req.headers.authorization assigned to a variable
2000
+ if (!/\breq\s*\.\s*headers\s*\.\s*authorization\b/.test(line))
2001
+ return false;
2002
+ // Check if Bearer is stripped in nearby lines
2003
+ const lineIdx = ctx.lineNumber - 1;
2004
+ const window = ctx.allLines
2005
+ .slice(lineIdx, Math.min(ctx.allLines.length, lineIdx + 3))
2006
+ .join(' ');
2007
+ // If it's passed directly to jwt.verify or used without .replace/.split/.slice
2008
+ const hasDirectUse = /jwt\s*\.\s*verify\b/.test(window);
2009
+ const hasStrip = /\.\s*(?:replace|split|slice|substring|startsWith)\b/.test(window) || /Bearer/i.test(window);
2010
+ return hasDirectUse && !hasStrip;
2011
+ },
2012
+ },
2013
+ // ════════════════════════════════════════════
2014
+ // Cloud Misconfiguration
2015
+ // ════════════════════════════════════════════
2016
+ {
2017
+ id: 'S3_PUBLIC_ACL',
2018
+ category: 'Cloud Misconfiguration',
2019
+ description: 'S3 bucket configured with public ACL (public-read, public-read-write) — bucket contents are exposed to the internet.',
2020
+ severity: 'high',
2021
+ fix_suggestion: 'Remove public ACL. Use S3 bucket policies with explicit access grants. Enable S3 Block Public Access at the account level.',
2022
+ auto_fixable: false,
2023
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
2024
+ skipCommentsAndStrings: false,
2025
+ skipTestFiles: true,
2026
+ detect: (line) => {
2027
+ return /\bACL\s*:\s*['"]public-read(?:-write)?['"]/.test(line) ||
2028
+ /\bpublic-read(?:-write)?\b/.test(line) && /\b(?:s3|bucket|putBucketAcl|PutBucketAcl|putObjectAcl)\b/i.test(line);
2029
+ },
2030
+ },
2031
+ {
2032
+ id: 'S3_CORS_PERMISSIVE',
2033
+ category: 'Cloud Misconfiguration',
2034
+ description: 'S3 CORS configuration allows all origins (AllowedOrigins: ["*"]) — any website can make cross-origin requests to this bucket.',
2035
+ severity: 'medium',
2036
+ fix_suggestion: 'Specify explicit allowed origins in S3 CORS configuration instead of using a wildcard.',
2037
+ auto_fixable: false,
2038
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
2039
+ skipCommentsAndStrings: false,
2040
+ skipTestFiles: true,
2041
+ detect: (line, ctx) => {
2042
+ // Match AllowedOrigins: ['*'] pattern
2043
+ if (!/AllowedOrigins\s*:\s*\[\s*['"][*]['"]\s*\]/.test(line))
2044
+ return false;
2045
+ // Verify it's in an S3/CORS context
2046
+ const lineIdx = ctx.lineNumber - 1;
2047
+ const window = ctx.allLines
2048
+ .slice(Math.max(0, lineIdx - 5), Math.min(ctx.allLines.length, lineIdx + 5))
2049
+ .join(' ');
2050
+ return /\b(?:CORS|CORSRules?|AllowedMethods|s3|bucket)\b/i.test(window);
2051
+ },
2052
+ },
2053
+ {
2054
+ id: 'NEXT_PUBLIC_SECRET',
2055
+ category: 'Sensitive Data Exposure',
2056
+ description: 'Environment variable with NEXT_PUBLIC_ prefix contains a secret-sounding name — NEXT_PUBLIC_ vars are exposed to the client bundle.',
2057
+ severity: 'high',
2058
+ fix_suggestion: 'Remove the NEXT_PUBLIC_ prefix from secret environment variables. Only use NEXT_PUBLIC_ for values safe to expose to the browser.',
2059
+ auto_fixable: false,
2060
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
2061
+ skipCommentsAndStrings: false,
2062
+ skipTestFiles: true,
2063
+ detect: (line) => {
2064
+ return /\bNEXT_PUBLIC_[A-Z_]*(?:SECRET|PRIVATE|PASSWORD|TOKEN|KEY|CREDENTIAL|AUTH)[A-Z_]*\b/.test(line);
2065
+ },
2066
+ },
2067
+ // ════════════════════════════════════════════
2068
+ // Supply Chain Attacks
2069
+ // ════════════════════════════════════════════
2070
+ {
2071
+ id: 'DYNAMIC_REQUIRE',
2072
+ category: 'Code Injection',
2073
+ description: 'Dynamic require() with user-controlled input — allows arbitrary module loading and code execution.',
2074
+ severity: 'critical',
2075
+ fix_suggestion: 'Never pass user input to require(). Use a whitelist/map of allowed modules: const allowed = { "a": require("./a") }.',
2076
+ auto_fixable: false,
2077
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
2078
+ skipCommentsAndStrings: true,
2079
+ skipTestFiles: true,
2080
+ detect: (line) => {
2081
+ // Match require(variable) where the argument is from user input
2082
+ return /\brequire\s*\(\s*req\s*\.\s*(?:body|query|params)\b/.test(line) ||
2083
+ /\brequire\s*\(\s*(?:userInput|input|moduleName|module|path)\s*\)/.test(line);
2084
+ },
2085
+ },
2086
+ {
2087
+ id: 'SUPPLY_CHAIN_POSTINSTALL',
2088
+ category: 'Supply Chain',
2089
+ description: 'Package script downloads and executes remote code (curl|sh, wget|bash) — a common supply chain attack vector.',
2090
+ severity: 'critical',
2091
+ fix_suggestion: 'Avoid downloading and executing scripts in package lifecycle hooks. Pin dependencies, use lockfiles, and audit package scripts.',
2092
+ auto_fixable: false,
2093
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
2094
+ skipCommentsAndStrings: false,
2095
+ skipTestFiles: true,
2096
+ detect: (line) => {
2097
+ // Match curl/wget piped to sh/bash in package.json scripts context
2098
+ return /\b(?:curl|wget)\s+[^\s|]+\s*\|\s*(?:sh|bash|zsh|node)\b/.test(line) ||
2099
+ /["'](?:postinstall|preinstall|install|prepare|prepublish)\s*["']\s*:\s*["'][^"']*(?:curl|wget)[^"']*\|\s*(?:sh|bash)\b/.test(line);
2100
+ },
2101
+ },
2102
+ // ════════════════════════════════════════════
2103
+ // Race Conditions
2104
+ // ════════════════════════════════════════════
2105
+ {
2106
+ id: 'RACE_CONDITION_NON_ATOMIC',
2107
+ category: 'Race Condition',
2108
+ description: 'Balance/inventory check followed by a separate update without a transaction or lock — vulnerable to race conditions that allow double-spending.',
2109
+ severity: 'high',
2110
+ fix_suggestion: 'Use a database transaction with SELECT ... FOR UPDATE, or use an atomic UPDATE with a WHERE clause (e.g., UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1).',
2111
+ auto_fixable: false,
2112
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
2113
+ skipCommentsAndStrings: false,
2114
+ skipTestFiles: true,
2115
+ detect: (line, ctx) => {
2116
+ // Detect SELECT balance/amount/quantity followed by UPDATE without transaction
2117
+ if (!/\b(?:SELECT|select)\b.*\b(?:balance|amount|quantity|inventory|stock|credits|points|remaining)\b/.test(line))
2118
+ return false;
2119
+ const lineIdx = ctx.lineNumber - 1;
2120
+ const window = ctx.allLines
2121
+ .slice(lineIdx, Math.min(ctx.allLines.length, lineIdx + 8))
2122
+ .join(' ');
2123
+ const hasUpdate = /\b(?:UPDATE|update)\b/.test(window);
2124
+ const hasTransaction = /\b(?:transaction|BEGIN|COMMIT|ROLLBACK|FOR UPDATE|LOCK|serialize|atomic|isolation)\b/i.test(ctx.fileContent);
2125
+ return hasUpdate && !hasTransaction;
2126
+ },
2127
+ },
2128
+ // ════════════════════════════════════════════
2129
+ // AI/LLM Security (expanded)
2130
+ // ════════════════════════════════════════════
2131
+ {
2132
+ id: 'AI_OUTPUT_EVAL',
2133
+ category: 'AI Security',
2134
+ description: 'AI/LLM model output passed to eval() — executing AI-generated code enables arbitrary code execution via prompt injection.',
2135
+ severity: 'critical',
2136
+ fix_suggestion: 'Never eval() AI-generated output. Use a sandboxed code execution environment (e.g., vm2, isolated-vm, Web Workers) or parse the output as structured data.',
2137
+ auto_fixable: false,
2138
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
2139
+ skipCommentsAndStrings: true,
2140
+ skipTestFiles: true,
2141
+ detect: (line, ctx) => {
2142
+ // Match eval() with AI-related variable names
2143
+ if (!/\beval\s*\(/.test(line))
2144
+ return false;
2145
+ return /\b(?:response|completion|result|output|generated|aiResponse|ai_response|message\.content|choices\[)/i.test(line) ||
2146
+ (() => {
2147
+ const lineIdx = ctx.lineNumber - 1;
2148
+ const window = ctx.allLines
2149
+ .slice(Math.max(0, lineIdx - 5), lineIdx + 1)
2150
+ .join(' ');
2151
+ return /\b(?:openai|anthropic|claude|gpt|completions|chat|llm|ai|model)\b/i.test(window);
2152
+ })();
2153
+ },
2154
+ },
2155
+ {
2156
+ id: 'AI_OUTPUT_HTML',
2157
+ category: 'AI Security',
2158
+ description: 'AI/LLM model output rendered as raw HTML (innerHTML) — enables XSS if the model output contains malicious HTML/scripts.',
2159
+ severity: 'high',
2160
+ fix_suggestion: 'Sanitize AI output with DOMPurify before rendering as HTML, or use textContent instead of innerHTML.',
2161
+ auto_fixable: false,
2162
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
2163
+ skipCommentsAndStrings: true,
2164
+ skipTestFiles: true,
2165
+ detect: (line, ctx) => {
2166
+ if (!/\.innerHTML\s*=/.test(line))
2167
+ return false;
2168
+ return /\b(?:completion|response|result|output|generated|aiResponse|choices\[|message\.content)\b/i.test(line) ||
2169
+ (() => {
2170
+ const lineIdx = ctx.lineNumber - 1;
2171
+ const window = ctx.allLines
2172
+ .slice(Math.max(0, lineIdx - 5), lineIdx + 1)
2173
+ .join(' ');
2174
+ return /\b(?:openai|anthropic|claude|gpt|completions|chat|llm|ai|model)\b/i.test(window);
2175
+ })();
2176
+ },
2177
+ },
2178
+ {
2179
+ id: 'AI_TOOL_INJECTION',
2180
+ category: 'AI Security',
2181
+ description: 'User input passed directly to LLM message content without filtering tool-use or function-call directives — enables tool/function injection.',
2182
+ severity: 'high',
2183
+ fix_suggestion: 'Filter user input for tool-use injection patterns (<tool_use>, function_call, <|im_start|>) before passing to LLM APIs. Validate message content and strip control sequences.',
2184
+ auto_fixable: false,
2185
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
2186
+ skipCommentsAndStrings: false,
2187
+ skipTestFiles: true,
2188
+ detect: (line, ctx) => {
2189
+ // Detect user input assigned to a variable and then passed as message content
2190
+ if (!/\brole\s*:\s*['"]user['"]/.test(line))
2191
+ return false;
2192
+ const lineIdx = ctx.lineNumber - 1;
2193
+ const window = ctx.allLines
2194
+ .slice(Math.max(0, lineIdx - 5), Math.min(ctx.allLines.length, lineIdx + 3))
2195
+ .join(' ');
2196
+ // Must have user input in the content field
2197
+ const hasUserInput = /content\s*:\s*(?:req\s*\.\s*(?:body|query|params)|userMsg|userMessage|userInput|input|message)\b/.test(window);
2198
+ // Must NOT have sanitization
2199
+ const hasSanitize = /\b(?:sanitize|filter|escape|validate|strip|clean)\b/i.test(window);
2200
+ return hasUserInput && !hasSanitize;
2201
+ },
2202
+ },
2203
+ {
2204
+ id: 'AI_PROMPT_LEAK',
2205
+ category: 'AI Security',
2206
+ description: 'System prompt or internal AI configuration exposed in an error response or API output — leaks proprietary instructions to users.',
2207
+ severity: 'high',
2208
+ fix_suggestion: 'Never include system prompts, internal configuration, or AI instructions in error responses. Log them server-side only.',
2209
+ auto_fixable: false,
2210
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
2211
+ skipCommentsAndStrings: true,
2212
+ skipTestFiles: true,
2213
+ detect: (line) => {
2214
+ // Match patterns where systemPrompt/system_prompt is sent in response
2215
+ return /\bres\s*\.\s*(?:json|send)\s*\([^)]*\b(?:systemPrompt|system_prompt|SYSTEM_PROMPT|instructions|system_message)\b/.test(line) ||
2216
+ /\b(?:prompt|systemPrompt|system_prompt|instructions)\s*:\s*\b(?:systemPrompt|system_prompt|SYSTEM_PROMPT)\b/.test(line) &&
2217
+ /\bres\s*\.\s*(?:json|send)\b/.test(line);
2218
+ },
2219
+ },
2220
+ {
2221
+ id: 'AI_FUNCTION_SCHEMA_INJECTION',
2222
+ category: 'AI Security',
2223
+ description: 'User input embedded in LLM function/tool calling schema (parameters, defaults) — enables manipulation of AI tool behavior.',
2224
+ severity: 'high',
2225
+ fix_suggestion: 'Never embed user input in function calling schemas. Define schemas statically and pass user input as message content only.',
2226
+ auto_fixable: false,
2227
+ fileTypes: ['.ts', '.tsx', '.js', '.jsx'],
2228
+ skipCommentsAndStrings: false,
2229
+ skipTestFiles: true,
2230
+ detect: (line, ctx) => {
2231
+ // Check for user input in function/tool schemas
2232
+ if (!/\b(?:default|description|enum)\s*:\s*(?:userInput|user_input|input|req\s*\.\s*(?:body|query|params))\b/.test(line))
2233
+ return false;
2234
+ // Verify this is in a functions/tools context
2235
+ const lineIdx = ctx.lineNumber - 1;
2236
+ const window = ctx.allLines
2237
+ .slice(Math.max(0, lineIdx - 10), Math.min(ctx.allLines.length, lineIdx + 3))
2238
+ .join(' ');
2239
+ return /\b(?:functions|tools|function_call|tool_choice|parameters)\b/.test(window);
2240
+ },
2241
+ },
1766
2242
  ];
1767
2243
  // ── File Discovery ──
1768
2244
  const MAX_FILES = 5_000;