| description | Guide for implementing JavaScript security queries in CodeQL |
|---|
This document provides specific, actionable instructions for implementing JavaScript security queries in CodeQL, focusing on common vulnerability patterns like XSS, SSRF, SQL Injection, and other security issues.
For Security Query Implementation (.ql/.qll files):
// Standard security query imports - USE THESE FOR ALL SECURITY QUERIES
import javascript
import semmle.javascript.dataflow.DataFlow
import semmle.javascript.dataflow.TaintTracking
import semmle.javascript.security.dataflow.RemoteFlowSources
import semmle.javascript.security.dataflow.UntrustedFlowSources
// Optional: Include path graph support for path-problem queries
// import DataFlow::PathGraphFor Framework-Specific Queries:
// Framework-specific imports as needed
import semmle.javascript.frameworks.Express
import semmle.javascript.frameworks.React
import semmle.javascript.frameworks.jQuery
import semmle.javascript.NodeJSCommon patterns for detecting user-controlled input:
// Express.js request parameters
class ExpressRequestSource extends RemoteFlowSource::Range {
ExpressRequestSource() {
this = any(Express::RequestInputAccess ria).getARootSource()
}
override string getSourceType() { result = "Express request parameter" }
}
// Generic request object patterns
class RequestInputSource extends RemoteFlowSource::Range {
RequestInputSource() {
exists(DataFlow::PropRead pr |
pr = this.asSource() and
pr.getBase().asExpr().(VarAccess).getName() = "req" and
pr.getPropertyName() in ["query", "params", "body", "headers"]
)
}
override string getSourceType() { result = "HTTP request input" }
}Patterns for detecting outgoing HTTP requests:
// Axios HTTP requests
class AxiosRequestSink extends DataFlow::Node {
AxiosRequestSink() {
exists(DataFlow::CallNode call |
call = DataFlow::moduleImport("axios").getAMemberCall(["get", "post", "put", "delete", "request"]) and
this = call.getArgument(0)
)
}
}
// Node.js native HTTP requests
class NodeHttpRequestSink extends DataFlow::Node {
NodeHttpRequestSink() {
exists(DataFlow::CallNode call |
call = DataFlow::moduleImport(["http", "https"]).getAMemberCall(["request", "get"]) and
this = call.getArgument(0)
)
}
}
// Fetch API requests
class FetchRequestSink extends DataFlow::Node {
FetchRequestSink() {
exists(DataFlow::CallNode call |
call = DataFlow::globalVarRef("fetch").getACall() and
this = call.getArgument(0)
)
}
}Patterns for detecting DOM manipulation that can lead to XSS:
// innerHTML and similar dangerous DOM properties
class DOMManipulationSink extends DataFlow::Node {
DOMManipulationSink() {
exists(DataFlow::PropWrite pw |
pw.getPropertyName() in ["innerHTML", "outerHTML"] and
this = pw.getRhs()
)
}
}
// Document write methods
class DocumentWriteSink extends DataFlow::Node {
DocumentWriteSink() {
exists(DataFlow::CallNode call |
call = DataFlow::globalVarRef("document").getAMemberCall(["write", "writeln"]) and
this = call.getAnArgument()
)
}
}Patterns for detecting SQL query construction:
// Generic database query patterns
class SqlQuerySink extends DataFlow::Node {
SqlQuerySink() {
exists(DataFlow::CallNode call |
call.getCalleeName() in ["query", "execute", "prepare", "run"] and
this = call.getArgument(0)
)
}
}
// String concatenation patterns (common SQL injection vector)
class SqlConcatenationSink extends DataFlow::Node {
SqlConcatenationSink() {
exists(DataFlow::CallNode call |
call.getCalleeName() in ["query", "execute"] and
this = call.getArgument(0) and
this.asExpr() instanceof AddExpr
)
}
}ALL security queries follow this exact pattern:
- Source Definition - Define what constitutes user input
- Sink Definition - Define what constitutes dangerous operations
- Flow Configuration - Define how data flows from sources to sinks
/**
* Provides sources, sinks and sanitizers for detecting [VULNERABILITY_NAME] vulnerabilities.
*/
module VulnerabilityName {
/** A data flow source for [VULNERABILITY_NAME] vulnerabilities */
abstract class Source extends DataFlow::Node { }
/** A data flow sink for [VULNERABILITY_NAME] vulnerabilities */
abstract class Sink extends DataFlow::Node { }
/** A sanitizer for [VULNERABILITY_NAME] vulnerabilities */
abstract class Sanitizer extends DataFlow::Node { }
// Use existing remote flow sources (PREFERRED)
private class RemoteFlowSourceAsSource extends Source instanceof RemoteFlowSource { }
// Define specific sinks based on vulnerability type
private class SpecificSinkAssink extends Sink {
SpecificSinkAsink() {
// Define sink patterns here
}
}
}/**
* Provides a taint-tracking configuration for detecting [VULNERABILITY_NAME] vulnerabilities.
*/
import javascript
import semmle.javascript.dataflow.DataFlow
import semmle.javascript.dataflow.TaintTracking
import VulnerabilityNameCustomizations::VulnerabilityName
module VulnerabilityNameConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { source instanceof Source }
predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
predicate isBarrier(DataFlow::Node node) { node instanceof Sanitizer }
// Add additional flow steps if needed
predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
// Custom flow steps for framework-specific patterns
none()
}
}
module VulnerabilityNameFlow = TaintTracking::Global<VulnerabilityNameConfig>;/**
* @name [Human-readable vulnerability name]
* @description [Description of the vulnerability and its impact]
* @kind path-problem
* @problem.severity [error|warning|recommendation]
* @security-severity [0.0-10.0]
* @precision [low|medium|high|very-high]
* @id [language]/[cwe-category]/[specific-id]
* @tags security
* external/cwe/cwe-[number]
*/
import javascript
import VulnerabilityNameQuery
import DataFlow::PathGraph
from VulnerabilityNameFlow::PathNode source, VulnerabilityNameFlow::PathNode sink
where VulnerabilityNameFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "[Alert message with $@ placeholder]",
source.getNode(), "[source description]"- RemoteFlowSource - User-controlled input from HTTP requests
- XssSink - DOM manipulation that can lead to script execution
- SqlQuerySink - Database query execution points
- HttpRequestSink - Outgoing HTTP requests (SSRF)
- CommandExecution - OS command execution
- FileSystemAccess - File read/write operations
- PathTraversal - File path manipulation
- CodeInjection - Dynamic code execution
| Vulnerability | Primary Sinks | Secondary Sinks |
|---|---|---|
| XSS | DOMManipulationSink, document.write |
Template rendering, HTML responses |
| SSRF | axios.get(), fetch(), http.request() |
URL construction |
| SQL Injection | db.query(), connection.execute() |
String concatenation patterns |
| Command Injection | child_process.exec(), eval() |
Template literal execution |
| Path Traversal | fs.readFile(), fs.writeFile() |
Path construction |
| Code Injection | eval(), Function(), vm.runInThisContext() |
Dynamic imports |
SSRF Detection Pattern:
private class RequestForgeryConfiguration extends TaintTracking::Configuration {
RequestForgeryConfiguration() { this = "RequestForgeryConfiguration" }
override predicate isSource(DataFlow::Node source) {
source instanceof RemoteFlowSource
}
override predicate isSink(DataFlow::Node sink) {
// HTTP client requests where URL is controllable
exists(DataFlow::CallNode call |
call = DataFlow::moduleImport("axios").getAMemberCall(["get", "post", "put", "delete"]) and
sink = call.getArgument(0)
) or
exists(DataFlow::CallNode call |
call = DataFlow::globalVarRef("fetch").getACall() and
sink = call.getArgument(0)
) or
exists(DataFlow::CallNode call |
call = DataFlow::moduleImport(["http", "https"]).getAMemberCall(["request", "get"]) and
sink = call.getArgument(0)
)
}
}XSS Detection Pattern:
private class ReflectedXssConfiguration extends TaintTracking::Configuration {
ReflectedXssConfiguration() { this = "ReflectedXssConfiguration" }
override predicate isSource(DataFlow::Node source) {
source instanceof RemoteFlowSource
}
override predicate isSink(DataFlow::Node sink) {
// DOM manipulation sinks
exists(DataFlow::PropWrite pw |
pw.getPropertyName() in ["innerHTML", "outerHTML"] and
sink = pw.getRhs()
) or
// Document write methods
exists(DataFlow::CallNode call |
call = DataFlow::globalVarRef("document").getAMemberCall(["write", "writeln"]) and
sink = call.getAnArgument()
)
}
}// NON_COMPLIANT: [Vulnerability description]
app.get('/vulnerable', (req, res) => {
const userInput = req.query.param; // Source: user input
vulnerableFunction(userInput); // Sink: dangerous operation
});
// COMPLIANT: [Safe alternative description]
app.get('/safe', (req, res) => {
const userInput = req.query.param;
const sanitized = sanitizeInput(userInput); // Sanitizer
safeFunction(sanitized); // Safe: input sanitized
});| file.js:line:col:line:col | Alert message with $@ | file.js:source_line:source_col:source_line:source_col | source description |
- Use specific call patterns: Prefer
DataFlow::CallNodeover genericCallExpr - Leverage framework libraries: Use Express, React modules for better precision
- Consider async patterns: JavaScript's async nature requires careful data flow analysis
- Handle dynamic property access: Account for bracket notation and computed properties
- Framework method chaining: Track fluent API calls through multiple steps