Static WordPress security scanners (WPScan, RIPS, Snyk) are good at what they do: matching code against catalogs of known vulnerability patterns. They miss the bugs that don't match a known pattern — the unusual permission check, the implicit trust between two functions, the data flow that crosses an unexpected boundary. LLM-based code review catches those, and adding it to the audit toolkit changes what kinds of bugs you find.
Every WordPress security audit I’ve run for the last decade has leaned on the same toolkit — WPScan for known plugin/theme CVEs, a dependency scanner like Snyk for the PHP and JS chain, sometimes a more exotic static analyzer like RIPS for SAST. These tools work the way you’d expect: they catalog known vulnerability patterns and they flag matches in your code. That’s valuable, and it should still be the first pass. It also fundamentally can’t find the bugs that don’t match a known pattern.
Adding LLM-based code review to that pass changes the shape of what the audit finds. Not because LLMs are smarter than the scanners — they’re not, and they hallucinate — but because they read code the way a senior reviewer reads code, and they can flag things that don’t show up in any signature database.
What static scanners catch.
Static scanners are excellent at:
- Known CVE patterns in third-party code (the plugin’s known vuln, the library’s known vuln)
- Common WordPress anti-patterns (missing nonce, missing capability check on a known action hook, unescaped output of a known function call)
- Dependency version mismatches
- Patterns that match a regex (SQL string concat near a user input, eval on a request param)
The thing they share: they’re matching against patterns someone already named. The catalog is finite. A bug that doesn’t fit a known pattern is invisible to them, no matter how obvious it would be to a human reviewer.
What LLMs catch that scanners don’t.
The class of bugs that show up under LLM review but not under static analysis:
- Logic bugs that depend on context. The function does a capability check — but the check is on a capability the calling user wouldn’t have anyway, so the check is effectively a no-op. The check matches the “capability check present” pattern; the LLM notices it doesn’t actually constrain anything.
- Implicit trust between functions. Function A sanitizes input. Function B calls function A and then trusts the output. Function C also calls function A and passes the output through a second sanitizer that interprets it differently. The two paths produce different security properties. Static analysis doesn’t see the relationship.
- Misuse of correct APIs. The code uses
wp_kses— the right escape function — but configures it with an allowed-tags list that includes<script>. Pattern match says “output is being sanitized.” LLM says “the sanitization is configured to allow exactly what you’re trying to prevent.” - Data-flow that crosses a privilege boundary. A function generates a list of post IDs from the current user’s posts. Another function takes a list of post IDs and operates on them, trusting that they’re already authorized. The two are now stitched together in a way that lets an attacker pass IDs they shouldn’t have access to. Each function in isolation is fine.
How this actually works in practice.
The workflow I’ve settled into for a serious audit:
- Run the static scanners first. WPScan against the installed plugin set. Dependency scanner against composer + npm. Sometimes a SAST tool against the custom theme/plugin code. Collect findings.
- Pass the custom code to an LLM with a security-review prompt. Ask it to find logic bugs, missing checks, and data-flow issues. It will produce a list that’s a mix of real findings and false positives.
- Verify every LLM finding by hand. This is the critical step. LLMs hallucinate, especially when they’re confident. Every finding gets read by a human who understands what the code is supposed to do, and either confirmed or discarded. About 30-50% of LLM findings are real on a typical pass.
- Combine into a single deliverable. Static-scanner findings, verified LLM findings, prioritized by severity and exploitability. The audit deliverable looks the same as a traditional audit; the contents are deeper.
What to be careful of.
Three failure modes specific to LLM-augmented audits:
- Trusting LLM confidence. An LLM will tell you confidently about a vulnerability that doesn’t exist. The grammar will be fluent, the explanation plausible, the line numbers correct, and the bug imaginary. Human verification on every finding isn’t optional.
- Missing the things LLMs are bad at. LLMs are weak at code that depends on global state, runtime configuration, or third-party data shape. A bug that only manifests because a specific WordPress hook fires in a specific order is harder to surface with code-only review. Static analysis sometimes catches these because of the call graph.
- Sending sensitive code to a third-party model. The convenience of running ChatGPT or Claude against your client’s custom code is real. The privacy implications of doing it are too. If the code is sensitive, use a self-hosted model or a vendor with an appropriate enterprise agreement.
LLM review is a complement to static analysis, not a replacement. Both have categories of bugs they catch and categories they miss. The serious audit uses both, verifies findings independently, and ships a single prioritized list. See security hardening for how this fits into a broader posture.