@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.
- package/dist/bin/shipsafe.js +2 -0
- package/dist/bin/shipsafe.js.map +1 -1
- package/dist/src/cli/scan-environment.d.ts +6 -0
- package/dist/src/cli/scan-environment.d.ts.map +1 -0
- package/dist/src/cli/scan-environment.js +81 -0
- package/dist/src/cli/scan-environment.js.map +1 -0
- package/dist/src/constants.d.ts +1 -1
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +20 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/engines/builtin/environment-scan.d.ts +45 -0
- package/dist/src/engines/builtin/environment-scan.d.ts.map +1 -0
- package/dist/src/engines/builtin/environment-scan.js +492 -0
- package/dist/src/engines/builtin/environment-scan.js.map +1 -0
- package/dist/src/engines/builtin/patterns.d.ts.map +1 -1
- package/dist/src/engines/builtin/patterns.js +476 -0
- package/dist/src/engines/builtin/patterns.js.map +1 -1
- package/dist/src/mcp/server.d.ts.map +1 -1
- package/dist/src/mcp/server.js +6 -0
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/mcp/tools/environment-scan.d.ts +3 -0
- package/dist/src/mcp/tools/environment-scan.d.ts.map +1 -0
- package/dist/src/mcp/tools/environment-scan.js +5 -0
- package/dist/src/mcp/tools/environment-scan.js.map +1 -0
- package/package.json +1 -1
|
@@ -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;
|