examples
Two small, runnable examples. Copy the files, run the command, get the same result.
semantic matching
A rule is a pattern that looks like the code it finds. Opengrep matches it against the syntax tree, not the text, so import aliases, line breaks, argument order and values held in variables don't get in the way.
shell-true.yaml
rules: - id: subprocess-shell-true languages: [python] severity: ERROR message: shell=True runs the command through a shell (command injection risk) pattern: subprocess.$FUNC(..., shell=True, ...)
deploy.py
import subprocessfrom subprocess import run as shdef deploy(branch): subprocess.call("git checkout " + branch, shell=True) sh(f"make release BRANCH={branch}", shell=True) use_shell = True subprocess.Popen( ["make", "clean"], cwd="/srv/app", shell=use_shell, ) subprocess.run(["make", "test"], shell=False)
output
$ opengrep scan -q --output-enclosing-context -f shell-true.yaml deploy.py
deploy.py
error subprocess-shell-true
shell=True runs the command through a shell (command injection risk)
in: function deploy
6: subprocess.call("git checkout " + branch, shell=True)
in: function deploy
8: sh(f"make release BRANCH={branch}", shell=True)
in: function deploy
11: subprocess.Popen(
12: ["make", "clean"],
13: cwd="/srv/app",
14: shell=use_shell,
15: )
- line 6the plain case:
shell=Truepassed directly. - line 8
shissubprocess.runimported under another name. - line 11spread over five lines, with
cwdbeforeshell, anduse_shellresolved toTrueby constant propagation. - line 17
shell=False, so no finding.
cross-function taint analysis
A taint rule says where untrusted data comes from (sources), where it must never end up (sinks) and what makes it safe (sanitizers). With the rule option taint_intrafile, Opengrep follows the data through function calls, return values, lambdas and higher-order functions.
command-injection.yaml
rules: - id: command-injection mode: taint languages: [javascript] severity: ERROR message: User input reaches a shell command options: taint_intrafile: true pattern-sources: - pattern: req.query - pattern: req.body pattern-sanitizers: - pattern: shellescape(...) pattern-sinks: - pattern: exec(...)
server.js
const express = require("express");const { exec } = require("child_process");const shellescape = require("shell-escape");const app = express();function tarCommand(dir) { return "tar czf /backups/latest.tgz " + dir;}function escape(dir) { return shellescape([dir]);}function backup(req, res) { exec(tarCommand(req.query.dir));}function backupAll(req, res) { req.body.dirs.map(tarCommand) .forEach((cmd) => exec(cmd));}function backupSafe(req, res) { exec(tarCommand(escape(req.query.dir)));}app.get("/backup", backup);app.post("/backup-all", backupAll);app.get("/backup-safe", backupSafe);
output
$ opengrep scan -q --dataflow-traces \
--output-enclosing-context -f command-injection.yaml server.js
server.js
error command-injection
User input reaches a shell command
in: function backup
16: exec(tarCommand(req.query.dir));
│
├─ source 16: exec(tarCommand(req.query.dir));
├─ through call 16: exec(tarCommand(req.query.dir));
│ ├─ param 7: function tarCommand(dir) {
│ └─ return 8: return "tar czf /backups/latest.tgz " + dir;
└─ sink 16: exec(tarCommand(req.query.dir));
in: function backupAll
21: .forEach((cmd) => exec(cmd));
│
├─ source 20: req.body.dirs.map(tarCommand)
├─ through call 21: .forEach((cmd) => exec(cmd));
└─ sink 21: .forEach((cmd) => exec(cmd));
- line 16
req.query.dirflows throughtarCommand()and its return value intoexec. - line 20each entry of
req.body.dirspasses throughmap(tarCommand)and a lambda before reachingexec. - line 25no finding: the input goes through
escape(), which calls the sanitizer.
flags
The commands above use these flags:
-qquiet: only the findings. Leaves out the banner, the progress and the summary that Opengrep prints around them.--output-enclosing-contextadds thein:line, naming the function, or the class and the method, that contains each finding.--dataflow-tracesdraws the path of the data under each taint finding, from the source to the sink. A call the data goes through opens into the steps inside the function, from the parameter it enters by (param) to thereturnit leaves by.