When Standard CodeQL Packs Aren't Enough: Building Custom Queries for Variant Analysis
Standard CodeQL packs catch common vulnerabilities, but your framework-specific wrappers and custom sanitizers need custom queries. Here's when to invest, a worked example for a Java SQL wrapper, and the trade-offs you'll face.
27 Jan 2026, 23:06 UTC

The Problem: Finding Variants of a Known Vulnerability
Your security team just patched a SQL injection in a legacy Java service. The fix was straightforward—parameterized queries replaced string concatenation. But the codebase spans 200+ microservices across three languages. How do you confidently find every similar pattern before the next release?
CodeQL's standard security packs catch common injection patterns, but they miss framework-specific wrappers, custom sanitizers, and architectural quirks unique to your codebase. This is where custom queries become necessary—not as a first step, but as a targeted investment after standard packs have done their job.
How CodeQL Differs from Pattern Matching
Traditional SAST tools grep for dangerous function calls like executeQuery(String). CodeQL instead builds a relational database of your code: abstract syntax trees, control-flow graphs, and data-flow edges become queryable tables. A query doesn't ask "does this line contain X?" It asks "does untrusted data reach this sink without passing through a known sanitizer?"
This semantic model enables variant analysis: write one query for the vulnerability pattern, run it across the entire codebase, and find every instance—including those hidden behind helper methods, builders, or framework abstractions.
Standard Packs First, Custom Queries Second
GitHub maintains query packs for each supported language (JavaScript/TypeScript, Python, Java, C/C++, C#, Go, Ruby, Swift). These cover OWASP Top 10 categories, CWE mappings, and quality rules. They're free for open source and included with GitHub Advanced Security (GHAS) for private repos.
Start here. Enable code scanning via the github/codeql-action in your CI pipeline. Let the standard packs run for a few sprints. Measure: how many alerts are true positives? How many are framework-specific false positives? Which vulnerability classes keep appearing in your triage backlog?
Only when standard packs consistently miss a pattern specific to your codebase should you invest in custom queries. The QL language (object-oriented, Datalog-inspired) has a 2–4 week learning curve for effective authoring. That investment pays off only when the query finds bugs standard packs cannot.
Worked Example: Custom Query for a Framework-Specific Wrapper
Suppose your Java services use a custom DatabaseExecutor wrapper that accepts raw SQL strings. Standard packs flag Statement.executeQuery(String) but miss calls to DatabaseExecutor.runRaw(String) because the sink isn't in the standard library model.
Create a custom query CustomSqlInjection.ql:
import java
import semmle.code.java.security.SqlInjection
class CustomSqlSink extends SqlSink {
CustomSqlSink() {
exists(MethodAccessCall mac |
mac.getMethod().hasQualifiedName("com.myorg.db", "DatabaseExecutor", "runRaw")
and
mac.getArgument(0) = this.getExpr()
)
}
}
from CustomSqlSink sink, DataFlow::PathNode source, DataFlow::PathNode sinkNode
where sinkNode.getNode() = sink
and source.getNode() instanceof RemoteFlowSource
select sink, source, sinkNode, "User-controlled data reaches custom SQL executor"
Where to run: Locally with CodeQL CLI (≥2.15) after creating a database: codeql database create /path/to/db --language=java --command="mvn compile". Then execute: codeql query run CustomSqlInjection.ql --database=/path/to/db --output=results.sarif.
Permissions: Read access to source code; write access to output directory. No elevated privileges needed.
Expected check: Open results.sarif in VS Code with the CodeQL extension or upload via github/codeql-action to see PR annotations. Verify the query flags executor.runRaw(userInput) but not executor.runParameterized(sql, params).
Risk: Overly broad sinks generate noise. Test against a known-good codebase first. SARIF uploads are limited to 10 MB per run—filter results if your monorepo exceeds this.
Trade-offs and Limitations
- False positives in complex frameworks: Spring, Django, and similar frameworks use reflection, AOP, and dynamic dispatch that break static data-flow analysis. You'll need to model sanitizers manually (e.g., annotate
@SafeSqlmethods) or accept noise. - No auto-fix: CodeQL alerts; it doesn't rewrite code. Remediation remains manual.
- GHAS cost model: Private repos require per-active-committer subscriptions. A 50-person team pays for 50 seats regardless of repo count.
- Build system gaps: Auto-build detection fails for Bazel, Gradle composite builds, or custom scripts. You'll often need explicit build commands in the workflow.
Actionable Next Steps
- Enable CodeQL code scanning on one representative repo using the standard action. Let it run for two weeks.
- Triage every alert. Tag false positives with framework-specific labels.
- If a vulnerability class appears repeatedly and standard packs miss it, write one custom query. Measure its precision before expanding.
- Promote high-value custom queries to an org-wide query pack so all repos inherit them.
Verify your setup: run codeql resolve languages to confirm supported languages, and codeql version to ensure you're on ≥2.15 for latest language packs. Check the CodeQL query reference for standard library predicates before writing custom logic.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.