Nine security anti-patterns your linter doesn't catch

Your linter is like a spell checker for code. Great at catching typos, terrible at catching lies.
It'll yell at you for a missing semicolon but won't blink at a SQL injection sitting three lines above it. That's not a bug in your linter - it's a scope problem. Linters enforce style. Security lives somewhere else entirely: in the gap between code that runs and code that should run.
These nine patterns are syntactically perfect. They compile. They pass review. They clear CI. And they'll happily ruin your week when someone finds them before you do.
Iris Code catches all nine on every save. Let's walk through them.
1. Dynamic code execution
eval() is the "hold my beer" of programming. It takes a string and runs it as code, which sounds powerful right up until a user-controlled query parameter becomes that string.
# user_filter arrives from a query parameter
result = eval(f"df[df.{user_filter}]")Some linters have optional rules for this. Most codebases disable them because somebody once had a "legitimate" use case in 2014 and nobody revisited the decision. In practice, every eval() deserves the same energy you'd give a stranger asking for your SSH keys.
2. SQL built with string concatenation
The year is 2026 and we're still concatenating user input into SQL strings. Bobby Tables graduated college by now.
const query = "SELECT * FROM users WHERE name = '" + name + "'";Your linter sees valid string concatenation. It doesn't know - and doesn't care - that the result is heading straight to a database driver with zero protection.
Parameterised queries exist. They've existed for decades. The dangerous pattern here isn't SQL - it's trusting user input to behave itself.
3. Insecure random number generation
Math.random() is fine for shuffling a playlist. It is absolutely not fine for generating session tokens, password reset codes, or anything that stands between an attacker and someone's account.
token := fmt.Sprintf("%d", rand.Intn(999999)) // math/rand is predictableGeneral-purpose PRNGs are designed to be fast, not unpredictable. That's the whole point. If randomness protects access, reach for the crypto shelf:
- Go:
crypto/rand - JavaScript:
crypto.getRandomValues() - Python:
secrets
Simple rule: if the random value guards a door, make sure it's not a door with a pattern lock.
4. ReDoS-prone regular expressions
Some regex patterns look innocent until a crafted input sends them into a backtracking spiral that eats your CPU alive. One request. One regex. One very bad afternoon.
The culprits are nested quantifiers:
(a+)+
(.*)*These create exponential matching paths. A 30-character input can take minutes to evaluate. Your linter doesn't parse regex internals - it just sees a valid string argument.
5. Hardcoded localhost URLs
Every developer has done this. You hardcode localhost:3000 during development because you'll "fix it later." Later never comes. It ships.
fetch("http://localhost:3000/api");In production, this means broken functionality, silent failures, or requests leaking to unexpected places. Configuration belongs in environment variables. Outside test files, a literal localhost address is a to-do that someone forgot to do.
6. Disabled TLS verification
const agent = new https.Agent({
rejectUnauthorized: false,
});This flag has a tragic origin story in every codebase. Someone couldn't get a self-signed cert working during local development, flipped it to false, left a // TODO: fix this comment, and moved on. That was two years ago. The comment is still there. The flag is still false. Man-in-the-middle attacks are still possible.
Equivalent sins in other languages:
verify=Falsein Python'srequestsInsecureSkipVerify: truein Go
If your HTTPS requests don't verify certificates, you're just doing HTTP with extra steps.
7. Debug mode left enabled
DEBUG = TrueFour characters. Infinite regret.
Debug mode in production hands attackers a guided tour of your application: stack traces, environment variables, file paths, configuration details, and occasionally actual credentials printed right there on the error page.
Debug mode is a development convenience. Shipping it to production is like leaving your house keys taped to the front door with a note that says "spare."
8. Weak hashing algorithms
MD5 was published in 1992. It was broken by 2004. It's 2026 and it's still showing up in production code.
password_hash = hashlib.md5(password.encode()).hexdigest()Modern hardware cracks unsalted MD5 hashes faster than you can alt-tab to check the results. SHA-1 isn't much better. Collision attacks against both are practical and well-documented.
Pick the right tool:
- Passwords: bcrypt, scrypt, or Argon2
- Integrity checks: SHA-256 or stronger
Your linter sees a valid function call to a legitimate library. It has no opinion on whether that library belongs in a museum.
9. Open redirects
const target = req.query.redirect;
res.redirect(target);Congrats, your trusted domain is now a phishing launchpad. An attacker crafts a link that starts at your legitimate URL and ends wherever they want. Victims see your domain in the link and click without thinking.
Always validate redirect destinations against an allowlist. If the destination isn't on the list, it doesn't happen.
Why linters miss all of this
None of these examples is a syntax error. Every single one is valid, idiomatic code. The risk isn't in how the code is written - it's in what the code does:
- Where did this input come from?
- Where is this value heading?
- What is it protecting?
- Is this API being used the way it was intended?
Linters match syntax patterns. They don't model trust boundaries, trace data flow, or understand context. Some security-focused lint rules exist, but enabling them on a mature codebase dumps hundreds of findings on you overnight, so teams disable them and move on.
Security detection needs a different approach: early, local, and scoped to the code you're actively changing.
Detection is only half the story
Iris Code flags all nine of these patterns on every save, directly in your editor, with analysis running entirely on your machine. No data leaves. No cloud round-trips.
But detection alone has a shelf life. Someone can still see the warning, shrug, and merge anyway. That's why Iris Code gives you two more layers:
Terminal scanning. iris security scans your project or staged files from the command line - on demand, in scripts, wherever you need it.
Enforceable quality gates. Set gateMaxSecuritySmells to 0 in your .irisconfig.json and security smells become a hard stop. Pre-push hooks fail. CI builds fail. No exceptions, no "it passed on my machine."
| Layer | Tool | When it runs |
|---|---|---|
| Editor | Iris Code sidebar | On every save |
| Terminal | iris security | On demand or in scripts |
| Enforcement | gateMaxSecuritySmells | Pre-push hooks and CI |
Commit the config once. Every developer on the team works against the same standard from that point forward.
Iris Code scores every file on save.
Get per-file health scores, security smell detection, and enforcement rules - directly in VS Code. Free to start, no account required.
Install the extension →