@neurolint/cli
Version:
NeuroLint CLI - Deterministic code fixing for TypeScript, JavaScript, React, and Next.js with 8-layer architecture including Security Forensics, Next.js 16, React Compiler, and Turbopack support
1,241 lines (1,076 loc) • 42.6 kB
JavaScript
/**
* Copyright (c) 2025 NeuroLint
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Layer 8: Security Forensics - Behavioral Analyzer
*
* AST-based detection engine for identifying suspicious code patterns
* beyond simple regex matching. Uses @babel/parser for deep code analysis.
*
* IMPORTANT: Layer 8 is READ-ONLY by default. It detects but does not transform
* unless explicitly requested (quarantine mode). This follows the NeuroLint
* principle of "never break code".
*/
'use strict';
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');
const path = require('path');
const { SEVERITY_LEVELS, IOC_CATEGORIES } = require('../constants');
const ErrorAggregator = require('../utils/error-aggregator');
class BehavioralAnalyzer {
constructor(options = {}) {
this.verbose = options.verbose || false;
this.maxDepth = options.maxDepth || 10;
this.includeContext = options.includeContext !== false;
this.findings = [];
this.currentFile = null;
this.currentCode = null;
this.errorAggregator = new ErrorAggregator({ verbose: this.verbose });
}
analyze(code, filePath, options = {}) {
this.findings = [];
this.currentFile = filePath;
this.currentCode = code;
this.lastFileErrors = [];
let ast;
try {
ast = this.parseCode(code, filePath);
} catch (error) {
const errorEntry = {
phase: 'parse',
file: filePath,
message: error.message
};
this.lastFileErrors.push(errorEntry);
this.errorAggregator.addError(error, errorEntry);
if (this.verbose) {
console.error(`[Layer 8] Parse error in ${filePath}: ${error.message}`);
}
const result = [...this.findings];
this.cleanup();
return result;
}
if (!ast) {
this.cleanup();
return this.findings;
}
try {
this.analyzeAST(ast, options);
} catch (error) {
const errorEntry = {
phase: 'ast-analysis',
file: filePath,
message: error.message
};
this.lastFileErrors.push(errorEntry);
this.errorAggregator.addError(error, errorEntry);
}
const result = [...this.findings];
this.cleanup();
return result;
}
cleanup() {
this.currentCode = null;
this.currentFile = null;
}
getLastFileErrors() {
return this.lastFileErrors || [];
}
resetForNewScan() {
this.findings = [];
this.currentCode = null;
this.currentFile = null;
this.lastFileErrors = [];
this.errorAggregator.clear();
}
parseCode(code, filePath) {
const ext = path.extname(filePath).toLowerCase();
// Skip JSON files - they can't be parsed with Babel's JavaScript parser
// JSON files are handled separately by the signature analyzer for regex patterns
if (ext === '.json') {
return null;
}
const isTypeScript = ['.ts', '.tsx'].includes(ext);
const isJSX = ['.jsx', '.tsx'].includes(ext);
const plugins = [
'decorators-legacy',
'classProperties',
'classPrivateProperties',
'classPrivateMethods',
'exportDefaultFrom',
'exportNamespaceFrom',
'dynamicImport',
'nullishCoalescingOperator',
'optionalChaining',
'optionalCatchBinding',
'objectRestSpread',
'asyncGenerators',
'functionBind',
'importMeta',
'topLevelAwait',
'bigInt',
'logicalAssignment',
'numericSeparator'
];
if (isTypeScript) {
plugins.push('typescript');
}
if (isJSX) {
plugins.push('jsx');
}
try {
return parser.parse(code, {
sourceType: 'module',
plugins,
allowImportExportEverywhere: true,
allowAwaitOutsideFunction: true,
allowReturnOutsideFunction: true,
allowSuperOutsideMethod: true,
errorRecovery: true
});
} catch (error) {
return parser.parse(code, {
sourceType: 'script',
plugins,
errorRecovery: true
});
}
}
analyzeAST(ast, options = {}) {
const self = this;
traverse(ast, {
CallExpression(nodePath) {
self.checkDangerousCalls(nodePath);
self.checkDynamicImports(nodePath);
self.checkProcessEnvExposure(nodePath);
self.checkNetworkRequests(nodePath);
self.checkChildProcess(nodePath);
self.checkCryptoMining(nodePath);
self.checkReact19Patterns(nodePath);
self.checkCVE202555184Patterns(nodePath);
self.checkCVE202555183Patterns(nodePath);
},
WhileStatement(nodePath) {
self.checkCVE202555184Patterns(nodePath);
},
ForStatement(nodePath) {
self.checkCVE202555184Patterns(nodePath);
},
DoWhileStatement(nodePath) {
self.checkCVE202555184Patterns(nodePath);
},
NewExpression(nodePath) {
self.checkFunctionConstructor(nodePath);
self.checkWebSocket(nodePath);
},
MemberExpression(nodePath) {
self.checkPrototypePollution(nodePath);
self.checkGlobalAccess(nodePath);
},
AssignmentExpression(nodePath) {
self.checkDangerousAssignments(nodePath);
self.checkPrototypeModification(nodePath);
},
Identifier(nodePath) {
self.checkDangerousIdentifiers(nodePath);
},
StringLiteral(nodePath) {
self.checkSuspiciousStrings(nodePath);
self.checkEncodedPayloads(nodePath);
},
TemplateLiteral(nodePath) {
self.checkTemplateInjection(nodePath);
},
ExportDefaultDeclaration(nodePath) {
self.checkServerActionPatterns(nodePath);
},
ExpressionStatement(nodePath) {
self.checkUseServerDirective(nodePath);
},
VariableDeclarator(nodePath) {
self.checkObfuscatedVariables(nodePath);
}
});
}
checkDangerousCalls(nodePath) {
const { node } = nodePath;
const callee = node.callee;
if (t.isIdentifier(callee)) {
if (callee.name === 'eval') {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-001',
signatureName: 'Direct eval() Call',
severity: SEVERITY_LEVELS.CRITICAL,
category: IOC_CATEGORIES.CODE_INJECTION,
description: 'Direct use of eval() detected - can execute arbitrary code',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Remove eval() and use safe alternatives like JSON.parse()'
});
}
if (callee.name === 'setTimeout' || callee.name === 'setInterval') {
if (node.arguments.length > 0 && t.isStringLiteral(node.arguments[0])) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-002',
signatureName: 'Timer with String Argument',
severity: SEVERITY_LEVELS.HIGH,
category: IOC_CATEGORIES.CODE_INJECTION,
description: `${callee.name}() with string argument - implicit eval`,
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Use function reference instead of string'
});
}
}
}
if (t.isMemberExpression(callee)) {
const obj = callee.object;
const prop = callee.property;
if (t.isIdentifier(obj, { name: 'document' }) &&
t.isIdentifier(prop, { name: 'write' })) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-003',
signatureName: 'document.write() Usage',
severity: SEVERITY_LEVELS.MEDIUM,
category: IOC_CATEGORIES.CODE_INJECTION,
description: 'document.write() can be used for XSS attacks',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Use DOM manipulation methods instead'
});
}
}
}
checkDynamicImports(nodePath) {
const { node } = nodePath;
if (t.isImport(node.callee)) {
const arg = node.arguments[0];
if (arg && !t.isStringLiteral(arg) && !t.isTemplateLiteral(arg)) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-004',
signatureName: 'Dynamic Import with Variable',
severity: SEVERITY_LEVELS.MEDIUM,
category: IOC_CATEGORIES.CODE_INJECTION,
description: 'Dynamic import with non-literal path - potential code injection',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Use static import paths when possible'
});
}
}
}
checkFunctionConstructor(nodePath) {
const { node } = nodePath;
if (t.isIdentifier(node.callee, { name: 'Function' })) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-005',
signatureName: 'Function Constructor',
severity: SEVERITY_LEVELS.HIGH,
category: IOC_CATEGORIES.CODE_INJECTION,
description: 'new Function() can execute arbitrary code',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Replace with static function definitions'
});
}
}
checkProcessEnvExposure(nodePath) {
const { node } = nodePath;
if (t.isMemberExpression(node.callee)) {
const calleeCode = this.getNodeCode(node.callee);
if (calleeCode.includes('Response.json') ||
calleeCode.includes('res.json') ||
calleeCode.includes('res.send')) {
const argCode = node.arguments.map(a => this.getNodeCode(a)).join('');
if (argCode.includes('process.env')) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-006',
signatureName: 'Environment Variable Exposure',
severity: SEVERITY_LEVELS.HIGH,
category: IOC_CATEGORIES.DATA_EXFILTRATION,
description: 'Environment variables being exposed via HTTP response',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Never expose environment variables in responses'
});
}
}
}
}
checkNetworkRequests(nodePath) {
const { node } = nodePath;
const callee = node.callee;
if (t.isIdentifier(callee, { name: 'fetch' }) ||
(t.isMemberExpression(callee) &&
t.isIdentifier(callee.property, { name: 'request' }))) {
const arg = node.arguments[0];
if (arg) {
const argCode = this.getNodeCode(arg);
if (/https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/.test(argCode)) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-007',
signatureName: 'Network Request to IP Address',
severity: SEVERITY_LEVELS.HIGH,
category: IOC_CATEGORIES.NETWORK,
description: 'Network request to raw IP address - potential C2 communication',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Use domain names instead of IP addresses'
});
}
if (argCode.includes('pastebin.com') ||
argCode.includes('raw.githubusercontent.com') ||
argCode.includes('iplogger') ||
argCode.includes('webhook.site')) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-008',
signatureName: 'Request to Suspicious Domain',
severity: SEVERITY_LEVELS.HIGH,
category: IOC_CATEGORIES.NETWORK,
description: 'Network request to known exfiltration/payload hosting domain',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Verify the legitimacy of this external request'
});
}
}
}
}
checkChildProcess(nodePath) {
const { node } = nodePath;
const callee = node.callee;
if (t.isMemberExpression(callee)) {
const propName = t.isIdentifier(callee.property) ? callee.property.name : null;
if (['exec', 'execSync', 'spawn', 'spawnSync', 'execFile', 'fork'].includes(propName)) {
const arg = node.arguments[0];
const argCode = arg ? this.getNodeCode(arg) : '';
if (argCode.includes('sh') || argCode.includes('bash') ||
argCode.includes('cmd') || argCode.includes('powershell') ||
argCode.includes('curl') || argCode.includes('wget')) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-009',
signatureName: 'Shell Command Execution',
severity: SEVERITY_LEVELS.CRITICAL,
category: IOC_CATEGORIES.BACKDOOR,
description: `${propName}() executing shell command`,
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Avoid shell command execution in application code'
});
}
}
}
}
checkCryptoMining(nodePath) {
const { node } = nodePath;
const callee = node.callee;
const calleeCode = this.getNodeCode(callee);
if (calleeCode.includes('CoinHive') ||
calleeCode.includes('coinhive') ||
calleeCode.includes('minero') ||
calleeCode.includes('cryptonight') ||
calleeCode.includes('wasm-miner')) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-010',
signatureName: 'Crypto Mining Activity',
severity: SEVERITY_LEVELS.CRITICAL,
category: IOC_CATEGORIES.CRYPTO_MINING,
description: 'Potential cryptocurrency mining code detected',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Remove cryptocurrency mining code immediately'
});
}
}
checkWebSocket(nodePath) {
const { node } = nodePath;
if (t.isIdentifier(node.callee, { name: 'WebSocket' })) {
const arg = node.arguments[0];
if (arg) {
const argCode = this.getNodeCode(arg);
if (/wss?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/.test(argCode)) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-011',
signatureName: 'WebSocket to IP Address',
severity: SEVERITY_LEVELS.HIGH,
category: IOC_CATEGORIES.NETWORK,
description: 'WebSocket connection to raw IP address - potential C2 channel',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Use domain names for WebSocket connections'
});
}
}
}
}
checkPrototypePollution(nodePath) {
const { node } = nodePath;
if (t.isIdentifier(node.property, { name: '__proto__' }) ||
t.isStringLiteral(node.property, { value: '__proto__' })) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-012',
signatureName: 'Prototype Access (__proto__)',
severity: SEVERITY_LEVELS.HIGH,
category: IOC_CATEGORIES.CODE_INJECTION,
description: 'Direct __proto__ access - potential prototype pollution',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Avoid direct prototype manipulation'
});
}
}
checkPrototypeModification(nodePath) {
const { node } = nodePath;
const left = node.left;
if (t.isMemberExpression(left)) {
const leftCode = this.getNodeCode(left);
if (leftCode.includes('.prototype.') ||
leftCode.includes('.__proto__') ||
leftCode.includes('[\'prototype\']') ||
leftCode.includes('["prototype"]')) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-013',
signatureName: 'Prototype Modification',
severity: SEVERITY_LEVELS.HIGH,
category: IOC_CATEGORIES.CODE_INJECTION,
description: 'Prototype chain being modified - potential prototype pollution attack',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Avoid modifying prototype chain'
});
}
}
}
checkGlobalAccess(nodePath) {
const { node } = nodePath;
if (t.isIdentifier(node.object, { name: 'global' }) ||
t.isIdentifier(node.object, { name: 'globalThis' }) ||
t.isIdentifier(node.object, { name: 'window' })) {
const propName = t.isIdentifier(node.property) ? node.property.name : null;
if (['eval', 'Function', 'constructor'].includes(propName)) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-014',
signatureName: 'Global Object Code Execution',
severity: SEVERITY_LEVELS.HIGH,
category: IOC_CATEGORIES.CODE_INJECTION,
description: `Accessing ${propName} via global object - evasion technique`,
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Remove global object access for code execution'
});
}
}
}
checkDangerousAssignments(nodePath) {
const { node } = nodePath;
const left = node.left;
if (t.isMemberExpression(left)) {
const leftCode = this.getNodeCode(left);
if (leftCode.includes('innerHTML') || leftCode.includes('outerHTML')) {
const rightCode = this.getNodeCode(node.right);
if (rightCode.includes('<script') ||
rightCode.includes('onerror') ||
rightCode.includes('onload')) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-015',
signatureName: 'XSS via innerHTML',
severity: SEVERITY_LEVELS.HIGH,
category: IOC_CATEGORIES.CODE_INJECTION,
description: 'Script or event handler injection via innerHTML',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Sanitize HTML content before assignment'
});
}
}
}
}
checkDangerousIdentifiers(nodePath) {
const { node } = nodePath;
const suspiciousNames = [
'exfiltrate', 'backdoor', 'payload', 'shellcode',
'keylogger', 'stealer', 'crypter', 'dropper'
];
const lowerName = node.name.toLowerCase();
for (const suspicious of suspiciousNames) {
if (lowerName.includes(suspicious)) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-016',
signatureName: 'Suspicious Variable Name',
severity: SEVERITY_LEVELS.MEDIUM,
category: IOC_CATEGORIES.BACKDOOR,
description: `Variable name contains suspicious term: ${suspicious}`,
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Investigate the purpose of this variable',
matchedText: node.name
});
break;
}
}
}
checkSuspiciousStrings(nodePath) {
const { node } = nodePath;
const value = node.value;
const suspiciousPatterns = [
{ pattern: /\/bin\/(?:sh|bash|zsh)/, name: 'Unix Shell Path' },
{ pattern: /cmd\.exe|powershell\.exe/, name: 'Windows Shell' },
{ pattern: /\\x[0-9a-f]{2}/gi, name: 'Hex Escape', minMatches: 5 },
{ pattern: /\\u[0-9a-f]{4}/gi, name: 'Unicode Escape', minMatches: 5 }
];
for (const { pattern, name, minMatches } of suspiciousPatterns) {
const matches = value.match(pattern);
if (matches && (!minMatches || matches.length >= minMatches)) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-017',
signatureName: `Suspicious String: ${name}`,
severity: SEVERITY_LEVELS.MEDIUM,
category: IOC_CATEGORIES.OBFUSCATION,
description: `String contains ${name} pattern`,
line: node.loc?.start.line,
column: node.loc?.start.column,
matchedText: value.substring(0, 100)
});
break;
}
}
}
checkEncodedPayloads(nodePath) {
const { node } = nodePath;
const value = node.value;
if (value.length > 500 && /^[A-Za-z0-9+/=]+$/.test(value)) {
try {
const decoded = Buffer.from(value, 'base64').toString();
if (decoded.includes('eval') ||
decoded.includes('Function') ||
decoded.includes('exec') ||
decoded.includes('<script')) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-018',
signatureName: 'Encoded Malicious Payload',
severity: SEVERITY_LEVELS.CRITICAL,
category: IOC_CATEGORIES.OBFUSCATION,
description: 'Base64 string decodes to potentially malicious code',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Investigate the encoded payload immediately'
});
}
} catch (e) {
}
}
}
checkTemplateInjection(nodePath) {
const { node } = nodePath;
for (const expr of node.expressions) {
const exprCode = this.getNodeCode(expr);
if (exprCode.includes('process.env') ||
exprCode.includes('req.') ||
exprCode.includes('request.')) {
const quasis = node.quasis.map(q => q.value.raw).join('');
if (quasis.includes('SELECT') ||
quasis.includes('INSERT') ||
quasis.includes('UPDATE') ||
quasis.includes('DELETE')) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-019',
signatureName: 'SQL Injection via Template',
severity: SEVERITY_LEVELS.CRITICAL,
category: IOC_CATEGORIES.CODE_INJECTION,
description: 'SQL query with user input via template literal',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Use parameterized queries instead'
});
}
}
}
}
checkServerActionPatterns(nodePath) {
const { node } = nodePath;
const parent = nodePath.parent;
if (parent && parent.type === 'Program') {
const body = parent.body;
const hasUseServer = body.some(n =>
n.type === 'ExpressionStatement' &&
n.expression.type === 'StringLiteral' &&
n.expression.value === 'use server'
);
if (hasUseServer && node.declaration) {
const funcBody = this.getNodeCode(node.declaration);
if (funcBody.includes('exec(') ||
funcBody.includes('spawn(') ||
funcBody.includes('eval(')) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-020',
signatureName: 'Dangerous Server Action',
severity: SEVERITY_LEVELS.CRITICAL,
category: IOC_CATEGORIES.RSC_SPECIFIC,
description: 'Server action contains dangerous code execution patterns',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Remove code execution from server actions',
references: ['CVE-2025-55182']
});
}
}
}
}
checkUseServerDirective(nodePath) {
const { node } = nodePath;
if (t.isStringLiteral(node.expression) &&
node.expression.value === 'use server') {
const unexpectedPaths = [
'components/', 'lib/', 'utils/', 'hooks/',
'public/', 'static/', 'assets/'
];
for (const unexpectedPath of unexpectedPaths) {
if (this.currentFile.includes(unexpectedPath)) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-021',
signatureName: 'use server in Unexpected Location',
severity: SEVERITY_LEVELS.MEDIUM,
category: IOC_CATEGORIES.RSC_SPECIFIC,
description: `Server action directive found in ${unexpectedPath}`,
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Server actions should be in app/ or actions/ directories',
references: ['CVE-2025-55182']
});
break;
}
}
}
}
checkObfuscatedVariables(nodePath) {
const { node } = nodePath;
const id = node.id;
if (t.isIdentifier(id)) {
const name = id.name;
if (/^_0x[a-f0-9]+$/i.test(name) || /^_[a-z]{1,2}\d{4,}$/i.test(name)) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-022',
signatureName: 'Obfuscated Variable Name',
severity: SEVERITY_LEVELS.MEDIUM,
category: IOC_CATEGORIES.OBFUSCATION,
description: 'Variable name suggests code obfuscation',
line: node.loc?.start.line,
column: node.loc?.start.column,
matchedText: name,
remediation: 'Deobfuscate code for security review'
});
}
}
}
checkReact19Patterns(nodePath) {
const { node } = nodePath;
const callee = node.callee;
if (t.isIdentifier(callee)) {
if (callee.name === 'use') {
const arg = node.arguments[0];
if (arg && t.isCallExpression(arg)) {
const innerCallee = arg.callee;
if (t.isIdentifier(innerCallee) && innerCallee.name === 'fetch') {
const fetchArgs = arg.arguments;
if (fetchArgs.length > 0) {
const urlArg = fetchArgs[0];
if (t.isTemplateLiteral(urlArg) && urlArg.expressions.length > 0) {
const hasTaintedExpr = urlArg.expressions.some(expr => {
const isTaintedSource = (node) => {
if (t.isCallExpression(node)) {
const nodeCallee = node.callee;
if (t.isMemberExpression(nodeCallee)) {
const obj = nodeCallee.object;
const prop = nodeCallee.property;
if (t.isIdentifier(obj) && t.isIdentifier(prop)) {
const objName = obj.name;
const propName = prop.name;
if ((objName === 'searchParams' || objName === 'formData') && propName === 'get') {
return true;
}
}
}
if (t.isIdentifier(nodeCallee) && nodeCallee.name === 'cookies') {
return true;
}
}
return false;
};
const getRootObject = (memberExpr) => {
let current = memberExpr;
while (t.isMemberExpression(current.object)) {
current = current.object;
}
return current.object;
};
const hasUserInputProperty = (memberExpr) => {
const props = [];
let current = memberExpr;
while (t.isMemberExpression(current)) {
if (t.isIdentifier(current.property)) {
props.push(current.property.name);
}
current = current.object;
}
return props.some(p => p === 'query' || p === 'body' || p === 'params');
};
if (isTaintedSource(expr)) {
return true;
}
if (t.isMemberExpression(expr)) {
const root = getRootObject(expr);
if (t.isIdentifier(root)) {
const rootName = root.name;
if ((rootName === 'req' || rootName === 'request' || rootName === 'context') &&
hasUserInputProperty(expr)) {
return true;
}
}
}
return false;
});
if (hasTaintedExpr) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-023',
signatureName: 'React 19 use() with User Input',
severity: SEVERITY_LEVELS.HIGH,
category: IOC_CATEGORIES.RSC_SPECIFIC,
description: 'React 19 use() hook with user-controlled URL in fetch',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Validate and sanitize inputs before constructing fetch URLs',
references: ['React 19 Security', 'CVE-2025-55182']
});
}
}
}
}
}
}
if (callee.name === 'useActionState') {
const actionArg = node.arguments[0];
if (actionArg) {
const actionCode = this.getNodeCode(actionArg);
if (actionCode.includes('eval') ||
actionCode.includes('exec') ||
actionCode.includes('spawn') ||
actionCode.includes('Function(')) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-024',
signatureName: 'React 19 useActionState with Code Execution',
severity: SEVERITY_LEVELS.CRITICAL,
category: IOC_CATEGORIES.RSC_SPECIFIC,
description: 'useActionState action contains code execution patterns',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Remove code execution from action handlers',
references: ['React 19 Security', 'CVE-2025-55182']
});
}
}
}
if (callee.name === 'useOptimistic') {
const updateFnArg = node.arguments[1];
if (updateFnArg) {
const updateCode = this.getNodeCode(updateFnArg);
if (updateCode.includes('dangerouslySetInnerHTML') ||
updateCode.includes('innerHTML') ||
updateCode.includes('eval')) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-025',
signatureName: 'React 19 useOptimistic XSS Risk',
severity: SEVERITY_LEVELS.HIGH,
category: IOC_CATEGORIES.CODE_INJECTION,
description: 'useOptimistic update function contains XSS-prone patterns',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Sanitize data in optimistic updates',
references: ['React 19 Security', 'OWASP XSS']
});
}
}
}
if (callee.name === 'startTransition') {
const transitionArg = node.arguments[0];
if (transitionArg) {
const transitionCode = this.getNodeCode(transitionArg);
if (transitionCode.includes('fetch') &&
(transitionCode.includes('process.env') ||
transitionCode.includes('cookies') ||
transitionCode.includes('headers'))) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-026',
signatureName: 'React 19 Transition Data Leak',
severity: SEVERITY_LEVELS.HIGH,
category: IOC_CATEGORIES.DATA_EXFILTRATION,
description: 'startTransition contains potential data exfiltration pattern',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Audit data handling in transitions',
references: ['React 19 Security']
});
}
}
}
if (callee.name === 'cache') {
const cacheArg = node.arguments[0];
if (cacheArg) {
const cacheCode = this.getNodeCode(cacheArg);
if (cacheCode.includes('cookies') ||
cacheCode.includes('headers') ||
cacheCode.includes('session') ||
cacheCode.includes('auth')) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-027',
signatureName: 'React 19 Server Cache Poisoning Risk',
severity: SEVERITY_LEVELS.HIGH,
category: IOC_CATEGORIES.RSC_SPECIFIC,
description: 'Server-side cache may store user-specific sensitive data',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Never cache user-specific or sensitive data',
references: ['React 19 Security', 'OWASP Cache Poisoning']
});
}
}
}
}
}
checkCVE202555184Patterns(nodePath) {
const { node } = nodePath;
if (t.isWhileStatement(node) || t.isForStatement(node) || t.isDoWhileStatement(node)) {
const test = node.test;
const isInfiniteLoop =
(t.isBooleanLiteral(test) && test.value === true) ||
(t.isNumericLiteral(test) && test.value !== 0) ||
(t.isIdentifier(test) && test.name === 'true') ||
(!test);
if (isInfiniteLoop) {
const hasServerDirective = this.currentCode &&
this.currentCode.includes("'use server'") ||
this.currentCode.includes('"use server"');
if (hasServerDirective) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-028',
signatureName: 'CVE-2025-55184 DoS: Infinite Loop in Server Context',
severity: SEVERITY_LEVELS.HIGH,
category: IOC_CATEGORIES.RSC_SPECIFIC,
description: 'Infinite loop detected in server action context - DoS vulnerability',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Add proper termination conditions to prevent server hang',
references: ['CVE-2025-55184', 'MITRE T1499']
});
}
}
}
if (t.isCallExpression(node)) {
const callee = node.callee;
if (t.isIdentifier(callee) &&
['setImmediate', 'queueMicrotask'].includes(callee.name)) {
const arg = node.arguments[0];
if (arg) {
const argCode = this.getNodeCode(arg);
if (argCode.includes(callee.name)) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-029',
signatureName: 'CVE-2025-55184 DoS: Recursive Async Scheduling',
severity: SEVERITY_LEVELS.HIGH,
category: IOC_CATEGORIES.RSC_SPECIFIC,
description: 'Recursive async scheduling can cause infinite CPU consumption',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Add termination conditions or iteration limits',
references: ['CVE-2025-55184']
});
}
}
}
if (t.isMemberExpression(callee) &&
t.isIdentifier(callee.object) &&
callee.object.name === 'process' &&
t.isIdentifier(callee.property) &&
callee.property.name === 'nextTick') {
const arg = node.arguments[0];
if (arg) {
const argCode = this.getNodeCode(arg);
if (argCode.includes('nextTick')) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-029',
signatureName: 'CVE-2025-55184 DoS: Recursive nextTick',
severity: SEVERITY_LEVELS.HIGH,
category: IOC_CATEGORIES.RSC_SPECIFIC,
description: 'Recursive process.nextTick can starve the event loop',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Add termination conditions',
references: ['CVE-2025-55184']
});
}
}
}
}
}
checkCVE202555183Patterns(nodePath) {
const { node } = nodePath;
if (!t.isCallExpression(node)) return;
const callee = node.callee;
if (t.isMemberExpression(callee) &&
t.isIdentifier(callee.property) &&
callee.property.name === 'toString') {
const hasServerDirective = this.currentCode &&
(this.currentCode.includes("'use server'") ||
this.currentCode.includes('"use server"'));
if (hasServerDirective) {
const objectCode = this.getNodeCode(callee.object);
if (objectCode.includes('function') ||
objectCode.includes('async') ||
objectCode.includes('=>')) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-030',
signatureName: 'CVE-2025-55183: Server Function Source Exposure',
severity: SEVERITY_LEVELS.HIGH,
category: IOC_CATEGORIES.RSC_SPECIFIC,
description: 'Function.toString() in server context exposes source code',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Never expose function source code from server actions',
references: ['CVE-2025-55183']
});
}
}
}
if (t.isIdentifier(callee) && callee.name === 'String') {
const arg = node.arguments[0];
if (arg) {
const argCode = this.getNodeCode(arg);
if (argCode.includes('function') || argCode.includes('=>')) {
const hasServerDirective = this.currentCode &&
(this.currentCode.includes("'use server'") ||
this.currentCode.includes('"use server"'));
if (hasServerDirective) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-031',
signatureName: 'CVE-2025-55183: Function Stringification in Server',
severity: SEVERITY_LEVELS.HIGH,
category: IOC_CATEGORIES.RSC_SPECIFIC,
description: 'String(function) in server context exposes source code',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Avoid converting functions to strings in server actions',
references: ['CVE-2025-55183']
});
}
}
}
}
if (t.isMemberExpression(callee) &&
t.isIdentifier(callee.property) &&
['json', 'send'].includes(callee.property.name)) {
const hasServerDirective = this.currentCode &&
(this.currentCode.includes("'use server'") ||
this.currentCode.includes('"use server"'));
if (hasServerDirective) {
const arg = node.arguments[0];
if (arg) {
const argCode = this.getNodeCode(arg);
if (argCode.includes('toString') ||
argCode.includes('.stack') ||
argCode.includes('Function')) {
this.addFinding({
signatureId: 'NEUROLINT-BEHAV-032',
signatureName: 'CVE-2025-55183: Response May Contain Source Code',
severity: SEVERITY_LEVELS.MEDIUM,
category: IOC_CATEGORIES.RSC_SPECIFIC,
description: 'Server response may contain function source code or stack traces',
line: node.loc?.start.line,
column: node.loc?.start.column,
remediation: 'Sanitize response data to remove source code and stack traces',
references: ['CVE-2025-55183']
});
}
}
}
}
}
getNodeCode(node) {
if (!node || !node.loc || !this.currentCode) return '';
const lines = this.currentCode.split('\n');
const startLine = node.loc.start.line - 1;
const endLine = node.loc.end.line - 1;
const startCol = node.loc.start.column;
const endCol = node.loc.end.column;
if (startLine === endLine) {
return lines[startLine]?.substring(startCol, endCol) || '';
}
let code = lines[startLine]?.substring(startCol) || '';
for (let i = startLine + 1; i < endLine; i++) {
code += '\n' + (lines[i] || '');
}
code += '\n' + (lines[endLine]?.substring(0, endCol) || '');
return code;
}
addFinding(finding) {
const context = this.includeContext ? this.getContext(finding.line) : [];
this.findings.push({
id: `finding-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
file: this.currentFile,
...finding,
context,
confidence: finding.confidence || 0.85,
timestamp: new Date().toISOString()
});
}
getContext(line, range = 3) {
if (!line || !this.currentCode) return [];
const lines = this.currentCode.split('\n');
const context = [];
const start = Math.max(0, line - range - 1);
const end = Math.min(lines.length, line + range);
for (let i = start; i < end; i++) {
context.push({
lineNumber: i + 1,
content: lines[i],
isMatch: i + 1 === line
});
}
return context;
}
getStats() {
const stats = {
total: this.findings.length,
bySeverity: {
critical: 0,
high: 0,
medium: 0,
low: 0,
info: 0
},
byCategory: {}
};
for (const finding of this.findings) {
stats.bySeverity[finding.severity] = (stats.bySeverity[finding.severity] || 0) + 1;
stats.byCategory[finding.category] = (stats.byCategory[finding.category] || 0) + 1;
}
return stats;
}
getErrors() {
return this.errorAggregator.toJSON();
}
clearErrors() {
this.errorAggregator.clear();
}
}
module.exports = BehavioralAnalyzer;