A team pushed code with a clean SonarQube scan. Three days later, their API was leaking customer PII through a path traversal bug. The scanner saw nothing wrong.
That's a false negative. Your tool said "safe" when the code was vulnerable. And unlike false positives that waste time, false negatives get you breached.
The Confidence Problem
Static analysis tools are great at finding patterns. eval(userInput)? Flagged. SELECT * FROM users WHERE id = '${req.params.id}'? Caught.
But throw in some indirection, and suddenly they're blind.
// Scanner catches this
const query = `SELECT * FROM users WHERE email = '${email}'`;
// Scanner misses this
const buildQuery = (field, value) => `SELECT * FROM users WHERE ${field} = '${value}'`;
const query = buildQuery(req.query.field, req.query.value);
The vulnerability is identical. The scanner only sees the second one as "dynamic string building" - not a SQL injection risk.
Real Bypasses That Work
1. Type Confusion
TypeScript gives tools a false sense of security. They trust the types.
interface SafeConfig {
allowedPaths: string[];
basePath: string;
}
function readFile(config: SafeConfig, filename: string) {
// Tool thinks: filename is string, basePath is string, this is safe
return fs.readFileSync(path.join(config.basePath, filename));
}
// Runtime: config comes from JSON.parse(untrustedInput)
// No validation. Attacker sends: {"basePath": "/etc", "allowedPaths": []}
// Tool never caught it because the type signature looked fine.
Production systems get owned this way. The types say "safe", the scanner believes it, and ../../../etc/passwd gets served.
2. Framework Magic
Modern frameworks do a lot behind the scenes. Scanners don't always understand the magic.
// Express route - looks safe to most scanners
app.get('/api/user/:id', async (req, res) => {
const user = await db.query('SELECT * FROM users WHERE id = ?', [req.params.id]);
res.json(user);
});
// Attacker finds: /api/user/1%00' OR '1'='1
// Framework decodes %00 to null byte
// Database driver (depending on version) treats it as string terminator
// Query becomes: SELECT * FROM users WHERE id = '' OR '1'='1'
// Scanner never flagged it - the parameterization looked correct.
Null byte injection is old. Still works. Still undetected by tools that only check surface syntax.
3. Async Timing Attacks
Some vulnerabilities aren't about what the code does - they're about when it does it.
async function validateToken(token: string): Promise {
const stored = await db.getToken(token);
if (!stored) return false;
// Timing leak: early return on length mismatch
if (token.length !== stored.length) return false;
for (let i = 0; i < token.length; i++) {
if (token[i] !== stored[i]) return false;
}
return true;
}
Character-by-character comparison. Each wrong character adds ~1ms. An attacker can brute-force tokens by measuring response time.
Zero static analysis tools flag this by default. It's not a code smell - it's a cryptographic weakness that requires behavioral analysis.
4. Prototype Pollution (The Silent Killer)
JavaScript's object model is a nightmare. Scanners struggle with it.
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
target[key] = merge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// Looks innocent. Used for config merging.
// Attacker sends: {"__proto__": {"isAdmin": true}}
// Now EVERY object in your app has isAdmin: true
// Scanner sees: recursive object merge. Perfectly normal.
Prototype pollution enabled RCE in multiple npm packages. Most scanners missed it because the pattern (recursive merge) is extremely common.
Why Tools Miss These
Path sensitivity. Following execution paths through async code, callbacks, and promise chains is computationally expensive. Most tools bail out after 2-3 levels of indirection.
Framework assumptions. Scanners assume frameworks do the right thing. Express sanitizes input. React escapes output. Except when they don't.
Domain knowledge. Timing attacks, cryptographic weaknesses, business logic flaws - these require understanding what the code is supposed to do, not just what it does.
Configuration blindness. A scanner can't see your nginx config, your database charset settings, or your cloud IAM roles. Vulnerabilities often live in the gap between code and infrastructure.
The False Negative Rate
Industry studies show 30-40% of real vulnerabilities go undetected by static analysis. That's not a tool problem - it's a fundamental limitation.
Tools find what they're programmed to find. New vulnerability classes (SSRF, IDOR, race conditions in distributed systems) take months or years to get patterns added.
By then, attackers are already exploiting them.
Layered Security Scanning
Single-tool scanning creates blind spots. ScanMyCode.dev runs multiple static analysis engines plus custom security rules to catch what standard tools miss - including indirect SQL injection, path traversal through complex logic, and framework-specific bypasses.
Results show exact locations, not just "file has issue." That's the difference between actionable and ignorable.
What Actually Works
Combine tools. SonarQube misses what Semgrep catches. CodeQL finds things ESLint ignores. Overlap matters.
Custom rules. Your codebase has patterns specific to your stack. Write rules for them. buildQuery() above? Needs a custom check.
Manual review for sensitive code. Auth logic, payment processing, admin functions - a human needs to read that. Tools are screening, not verdict.
Dynamic testing. If static analysis was enough, pen testing wouldn't exist. You need both.
False negatives are the security debt nobody talks about. Green pipelines don't mean secure systems. Run a multi-engine security audit and find out what your current setup is missing.