Application Security10 min read2026-10-17

Inside Semgrep: Declarative Pattern Matching and Taint Tracking for Web Applications

How Semgrep provides high-speed Static Application Security Testing (SAST). Writing custom rules, tracking untrusted user input, and intercepting OWASP Top 10 vulnerabilities.

AS
Ankur Sharma
Principal Security Architect • DevAudit Research Team
#Semgrep#SAST#Security#OWASP#Code Quality

The Limitations of Heavyweight Legacy SAST

Traditional enterprise Static Application Security Testing (SAST) tools historically earned a reputation for being slow and cumbersome. Engineers spent hours configuring complex build environments, only to wait 45 minutes for a scan that returned hundreds of noisy false positives.

**Semgrep** emerged as a lightweight, fast alternative. By parsing source code directly into language-agnostic Abstract Syntax Trees and querying them using familiar source code patterns, Semgrep executes scans in sub-seconds.


How Semgrep Pattern Matching Works

Instead of learning proprietary query languages (such as Datalog or CodeQL), Semgrep rules resemble the code you are searching for.

Consider searching for insecure instances of Node.js `eval()`:

yaml
rules:
  - id: no-eval
    languages: [javascript, typescript]
    message: "Avoid dynamic code execution with eval(); it enables Remote Code Execution (RCE)."
    severity: ERROR
    pattern: eval(...)

The ellipsis operator (`...`) is an abstraction that matches: - Zero or more arguments inside a function call. - Zero or more statements inside a block. - Any arbitrary expressions.

Semgrep recognizes that `eval("console.log('test')")` and `eval(userPayload, extraContext)` both match `pattern: eval(...)`, regardless of formatting, indentation, or comments.


The Power of Metavariables

Metavariables (prefixed with `$`, such as `$USER_INPUT` or `$OBJ`) capture expressions and enforce matching across different locations in code:

yaml
# Detect useless self-assignment: x = x
rules:
  - id: suspicious-self-assignment
    languages: [javascript, typescript, python]
    message: "Suspicious self-assignment detected: $VAR is assigned to itself."
    severity: WARNING
    pattern: $VAR = $VAR

If Semgrep encounters `user.age = user.age`, `$VAR` binds to `user.age`, and a vulnerability alert is triggered immediately.


Taint Analysis: Tracing Untrusted Data from Source to Sink

The real power of modern SAST lies in **Taint Tracking**. In web security: - A **Source** is any entry point where untrusted user input enters the system (e.g., `req.query.id`, `req.body`, `request.form`). - A **Sink** is a security-sensitive function where untrusted data could cause harm (e.g., `db.query()`, `eval()`, `res.send()`). - A **Sanitizer** is a function that validates, escapes, or cleans the data (e.g., `parseInt()`, `DOMPurify.sanitize()`).

mermaid
flowchart LR
    Source["Source: req.query.id (Untrusted Input)"] --> Path["Data Flow Path (Propagation)"]
    Path --> Sink["Sink: db.query(...) (SQL Injection)"]
    Path -. Sanitizer .-> Clean["Sanitizer: parseInt(id) (Safe)"]

#### Writing a Taint Rule in Semgrep

Here is an example rule detecting SQL Injection in PostgreSQL queries:

yaml
rules:
  - id: node-postgres-sqli
    languages: [javascript, typescript]
    severity: ERROR
    message: "Detected potential SQL Injection: untrusted HTTP input reaches raw query string."
    mode: taint
    pattern-sources:
      - pattern: req.query.$PARAM
      - pattern: req.body.$PARAM
      - pattern: req.params.$PARAM
    pattern-sanitizers:
      - pattern: parseInt(...)
      - pattern: validator.escape(...)
    pattern-sinks:
      - pattern: db.query($QUERY, ...)
      - pattern: client.query($QUERY, ...)

When a developer executes:

typescript
app.get('/api/users', async (req, res) => {
  const userId = req.query.id; // SOURCE
  
  // High risk: Raw string concatenation directly into SQL query!
  const query = `SELECT * FROM users WHERE id = '${userId}'`;
  const result = await db.query(query); // SINK! Alert triggered!
  
  res.json(result.rows);
});

Semgrep traces the tainted variable `userId` from the HTTP request query into the template string and finally into the database query sink.


Integrating Semgrep with DevAudit

The DevAudit container architecture runs an isolated microservice container (`devaudit-semgrep`) that audits pull request diffs and repository snapshots on demand: - **Zero Configuration**: Common rulesets (OWASP Top 10, Secrets, Node.js security) are bundled by default. - **Fast Execution**: Ephemeral checks complete in milliseconds on typical commits. - **Direct Feedback**: Generates actionable annotations that explain both why the defect occurred and how to remediate it.

TEST THIS CODE IN LIVE ENVIRONMENT

Run AST & Dependency Analysis On Your Code

Inspect your repository against the Google OSV API, Semgrep SAST engine, and SPDX compliance rules in sub-seconds.

Scan Repository Now