Detection Engineering

SQL Injection Detection: A Defensive Guide

How to detect SQL injection across web, app, and database telemetry — with Sigma, Suricata, and SPL rules, a CVE-2025-1094 case study, and tuning tips. Lab-only.

A SIEM log table with several SQL-error rows highlighted in red against fine cyan text

SQL injection detection works on three layers at once: the web request, the application error stream, and the database engine’s own behavior. A WAF signature alone misses blind and second-order attacks. The durable signal is correlation — a spike of database syntax errors from one source IP, an application account suddenly running UNION SELECT, or a database process spawning a shell. This guide ships the rules to catch all three, plus how to tune and test them.

SQL injection still sits at the top of the OWASP Top 10 as A03:2021 — Injection, and it still ends up in nation-state breach chains. In February 2025, Rapid7 disclosed CVE-2025-1094, a PostgreSQL injection flaw that was a required link in the BeyondTrust exploit chain behind the US Treasury intrusion. We use it as the worked example, because it breaks an assumption most teams still hold.

What is SQL injection, in a defender’s terms?

SQL injection is an attack where untrusted input changes the structure of a database query instead of just its data. The application meant to ask “find user 123”; the attacker rewrites the question into “find user 123, and also dump every password.” You do not need to write payloads to detect them — you need to know the shapes they leave behind in your telemetry.

A successful injection rarely stays “just” a data leak. The same flaw that dumps a users table can pivot to code execution through database features like xp_cmdshell on SQL Server or psql meta-commands on PostgreSQL. That is the difference between an incident report and a breach notification, and it is why detection has to reach the database tier, not stop at the WAF.

What are the four types of SQL injection?

There are four families, and each leaves a different telemetry fingerprint. The table below is the cheat sheet I keep next to the SIEM — it maps each variant to the signal it produces and the layer that catches it best.

VariantWhat the attacker doesTelemetry fingerprintBest detection layer
Error-basedProvokes verbose DB errors to leak schema/dataBurst of HTTP 500s + DB syntax errors from one IPWeb / app logs
Union-basedAppends attacker columns onto a real result setResponse row count or byte size far above baselineNetwork + DB
Boolean-blindAsks true/false questions one bit at a timeMany near-identical requests, two alternating sizesWeb behavioral
Time-based blindInjects a sleep, reads the answer from latencyRepeated requests with round-number latency (5.0s)Web behavioral

The lesson for detection design: signatures catch the loud variants (error- and union-based), but blind injection only shows up in behavioral and timing analytics. You need both, which is exactly why this guide layers them.

How to detect SQL injection across three layers

Layer the detection the way the attack surface is layered: web, network, and the database tier most teams skip. The same philosophy drives every rule on darkpwn — behavior over brittle strings, the way the detection-as-code workflow for Sigma rules lays out.

Web tier: signature and rate detection

The cheapest sensor is the access log. The Sigma rule below flags injection tokens as a triage signal for correlation — the WAF does the blocking, this gives your SIEM the event to correlate.

Sigma Web Request Containing SQL Injection Tokens
title: Web Request Containing SQL Injection Tokens
id: 7b3f1d2a-darkpwn-illustrative
status: experimental
logsource:
  category: webserver
detection:
  selection:
    cs-uri-query|contains:
      - 'union select'
      - "' or 1=1"
      - 'sleep('
      - 'pg_sleep('
      - 'waitfor delay'
      - 'information_schema'
  condition: selection
falsepositives:
  - Security scanners and DAST runs sending benign test strings
level: medium

That single-event rule is noisy alone. The higher-fidelity web signal is rate: count database syntax errors per source IP per minute — the error-based fuzzing fingerprint — and only alert above a threshold. A login form that throws 10 SQL syntax errors a minute from one IP is being fuzzed, not used.

Network tier: a Suricata signature for union-based attacks

If you run an IDS between tiers, a Suricata rule catches the union pattern on the wire before it reaches the app. Reserve SID range 1000001+ for your own rules.

Suricata UNION SELECT in HTTP Request
alert http any any -> $HTTP_SERVERS any (
    msg:"DARKPWN SQLi UNION SELECT in HTTP request";
    flow:established,to_server;
    http.uri; content:"union"; nocase;
    content:"select"; nocase; distance:0; within:30;
    classtype:web-application-attack; sid:1000001; rev:1;)

Network rules see only what isn’t TLS-terminated upstream. If your edge terminates TLS, run Suricata against the decrypted span or lean on layers 1 and 3 — and write the gap into your coverage matrix so a blind spot doesn’t read as coverage.

Database tier: the detection most teams skip

This is the highest-value layer and the one most often missing. A database engine has no business launching a shell — when it does, treat it as an incident, not an alert. This is the post-exploitation stage CVE-2025-1094 reached.

Sigma Database Process Spawned a Command Shell
title: Database Process Spawned a Command Shell
id: 2d8a4c11-darkpwn-illustrative
status: experimental
logsource:
  category: process_creation
detection:
  parent:
    ParentImage|endswith: ['\sqlservr.exe', '\postgres.exe', '\mysqld.exe', '\psql.exe']
  child:
    Image|endswith: ['\cmd.exe', '\powershell.exe', '\bash', '\sh']
  condition: parent and child
falsepositives:
  - Rare DBA jobs or backup scripts that shell out (allowlist them)
level: high

The correlated detections in SPL and KQL

The callout above names the three high-fidelity signals; here they are as deployable queries rather than advice.

SPL Error-Rate Spike and Result-Volume Anomaly (Splunk)
index=db sourcetype=db:audit
| bin _time span=1m
| stats count AS queries,
        sum(eval(if(status="error", 1, 0))) AS errors,
        avg(rows_returned) AS avg_rows,
        dc(query_hash) AS distinct_shapes by _time, db_user, src_ip
| eventstats median(avg_rows) AS baseline_rows by db_user
| eval error_rate = round(errors * 100.0 / queries, 1)
| eval finding = case(
    errors >= 10,                                    "error_based_fuzzing",
    avg_rows > (baseline_rows * 50),                 "union_exfiltration",
    distinct_shapes > 30,                            "query_shape_anomaly",
    true(), null())
| where isnotnull(finding)
| table _time, db_user, src_ip, finding, errors, error_rate, avg_rows, baseline_rows
KQL Application Account Executing Out-of-Baseline SQL Verbs
let baselineDays = 14d;
// What verbs does each application account normally use?
let baseline =
    DatabaseAudit
    | where TimeGenerated between (ago(baselineDays) .. ago(1h))
    | summarize KnownVerbs = make_set(Verb) by DbUser;
DatabaseAudit
| where TimeGenerated > ago(1h)
| where AccountType == "application"
| join kind=inner baseline on DbUser
| where Verb !in (KnownVerbs)
| extend Severity = iff(Verb in ("DROP","ALTER","GRANT","EXEC","xp_cmdshell"),
                        "Critical", "High")
| project TimeGenerated, DbUser, Verb, Statement, SourceHost, Severity
| order by TimeGenerated desc

The second query encodes an underused property of application database accounts: they are predictable in a way human accounts are not. A web application’s service account issues a small, stable set of query shapes, and it has issued them thousands of times. The first DROP, GRANT, or EXEC that account has ever run is not a threshold judgement — it is a categorical anomaly, and it makes an excellent alert with almost no tuning.

How do you set these thresholds?

Derive each from your own baseline rather than adopting the numbers above:

  • Error rate. Measure database syntax errors per minute per source over 30 days. Normal applications produce almost none — errors mean malformed SQL, and an application generating malformed SQL in steady state has a bug worth fixing anyway. Start near the 99th percentile.
  • Result-volume anomaly. Compare against the median rows returned for that query shape, not the mean. Means are dragged upward by legitimate large reports; medians are not.
  • Query-shape count. Normalize each statement to a shape by stripping literals, then count distinct shapes per account per window. A stable application has a small, unchanging set.

What telemetry do you need to detect SQL injection?

LayerSourceWhat it catchesThe usual gap
WebAccess logs with full query string and body sizeLoud injection attemptsBodies not logged; POST payloads invisible
NetworkIDS with TLS visibilityUnion and error payloads on the wireNo TLS termination point to inspect at
DatabaseNative audit log or proxyThe successful attack and its impactAlmost never enabled; cost concerns
HostProcess creation on the DB serverPost-exploitation shellNo agent on database hosts

The database row is where most programmes are blind, and it is the layer that distinguishes an attempt from a breach. Web-tier signatures tell you somebody tried; database auditing tells you what they got. If enabling full statement auditing is too expensive, audit selectively — privileged verbs, schema changes, and failed statements — which captures the signal at a fraction of the volume.

Host telemetry on database servers is the second common gap. Database hosts are frequently excluded from endpoint agent rollouts for performance reasons, which is precisely why the “database spawned a shell” rule above so often has no data behind it.

Which false positives will you actually see?

False positiveWhich rule it hitsWhy it happensResolution
DAST scans and pen testsWeb signatureThey send injection payloads deliberatelyAllowlist source IPs; verify the rules fired
Legitimate SQL in user contentWeb signatureForums, code snippets, bug reports contain SQLScope to parameters that never carry prose
Reporting and analytics queriesResult-volume anomalyLarge reads are the featureBaseline per query shape, not per account
Schema migrationsVerb anomalyDeployments legitimately run DDLCorrelate with the deploy pipeline; alert on unmatched
ORM query variationQuery-shape countDynamic filters produce many shapesNormalize by stripping literals before counting
Backup and maintenance jobsDB-spawns-shellSome backup tooling genuinely shells outAllowlist the specific parent/child pair and account
Application error stormsError-rate spikeA bug produces malformed SQL at volumeFix the bug; it is a real defect either way
Health-check probesWeb signatureMonitoring hits endpoints with odd parametersAllowlist the monitoring source

The schema-migration row is the one to wire properly rather than suppress. Correlating DDL against your deployment pipeline turns a noisy exclusion into a strong detection: a schema change that matches a deploy is routine, and one that does not is exactly the alert you want.

How do you triage a SQL injection alert?

  1. Establish which layer fired. A web-tier signature is an attempt. A database verb anomaly or a shell spawned from the database process is a successful compromise.
  2. If the database tier fired, treat it as an incident immediately. A database engine launching a shell is not a tuning conversation.
  3. Pull every statement that account executed in the window. This is where database auditing pays for itself — it is the difference between knowing what was accessed and guessing.
  4. Quantify the read volume. Rows returned tells you the scale of the exposure and directly informs breach-notification obligations.
  5. Identify the vulnerable parameter. Correlate the database statement timestamp against web access logs to find the endpoint and parameter that carried the payload.
  6. Check for persistence. New database users, new grants, modified stored procedures, scheduled jobs, and — where the shell succeeded — web shells written to disk.
  7. Rotate database credentials and any secrets the account could read.
  8. Fix the query, then verify the fix by parameter. Patching one endpoint while the same string-concatenation pattern exists elsewhere in the codebase leaves the door open.

Step 8 generalizes. SQL injection is rarely a lone defect; it reflects a pattern in how that codebase builds queries. After remediation, grep the repository for the concatenation pattern that caused it — the same bug is usually present in several places written by the same hand.

How to test your SQL injection detection

A detection you have never watched fire is a hypothesis, not a control. Validate each rule in a lab you own before you trust it in production:

  1. Stand up a deliberately vulnerable app — OWASP Juice Shop or DVWA in a throwaway VM, never against a system you don’t own.
  2. Generate each variant against it: error-based, union-based, and both blind types. Confirm the matching rule fires on the true positive.
  3. Replay a normal baseline — real user traffic, scanners you’ve allowlisted, your own DAST run — and confirm the rule stays quiet. A rule that fires on your weekly scan will be muted within a week.
  4. Tune the thresholds to your endpoint’s real error rate, then re-run both passes. Record the false-positive sources in the rule itself.

This is the same fire-it-before-the-adversary-does discipline behind Sigma rules that actually fire.

How to prevent SQL injection

Detection buys you time. Prevention removes the bug class. Deploy both, in this order of impact.

  • Treat the WAF as defense in depth, not the control. Encoding tricks and blind variants slip past; CVE-2025-1094 had nothing to match against.
  • Patch the data tier, not just the app tier. CVE-2025-1094 was fixed in PostgreSQL 17.3, 16.7, 15.11, 14.16, and 13.19. Track database-engine and driver CVEs with the same urgency as application CVEs.
  • Validate input at the boundary as a noise reducer, never as the only line — an integer ID should be an integer.

Which control buys the most?

ControlRemoves the bug class?EffortNotes
Bound parametersYes, for first-orderLow per call siteThe only real fix; mechanical to review
Least-privilege DB accountNo — caps the damageLowDecides whether a leak becomes a shell
Disable xp_cmdshell / restrict FILENo — removes the pivotVery lowTurns RCE back into a data issue
Patch database engine and driversCloses specific flawsLow, recurringCVE-2025-1094 lived below the app tier
Database audit loggingNo — makes it visibleMediumThe difference between “we think” and “we know”
ORM used correctlyYes, mostlyLowRaw-string escape hatches are the exception
Input validationPartiallyMediumGood hygiene, brittle as a control
WAF signaturesNoLowDefense in depth; gives false confidence alone

Read that table top-down when deciding where to spend. Rows one and three together are days of work and they remove both the vulnerability class and its worst outcome. The bottom row is worth running and worth never relying on — CVE-2025-1094 is the standing demonstration, because the payload was validly escaped output and there was nothing for a content-matching WAF to match.

On ORMs specifically: an ORM used normally parameterizes for you and closes this class by default. The risk lives entirely in the escape hatches — raw query methods, string-interpolated WHERE fragments, and dynamic ORDER BY clauses, which cannot be parameterized in most drivers and so get built by concatenation. Grep for the raw-query method names in your codebase; that short list is your realistic SQL injection surface, and it is usually small enough to review by hand in an afternoon.

Common SQL injection detection mistakes

These are the gaps I see most often when reviewing a SIEM that “already covers SQLi”:

  • WAF-only coverage. The WAF blocks the obvious and logs nothing useful about the rest. Forward app and database telemetry too.
  • No database-tier visibility. Without process-creation and query-audit logs, the highest-fidelity signal — a DB engine spawning a shell — is invisible.
  • Ignoring second-order injection. The payload is stored clean and fires later from a different code path. Request-time rules see nothing; only DB verb/volume anomalies catch it.
  • Untuned thresholds. A rule that pages on every DAST scan gets muted, and a muted rule detects nothing.

SQL injection detection checklist

Copy this into your detection backlog and check it off:

  1. Parameterize every query; ban string concatenation in code review.
  2. Run the app’s DB account at least privilege — no FILE, no xp_cmdshell.
  3. Forward web access logs, application error logs, and DB audit logs to the SIEM.
  4. Deploy the web-token Sigma rule plus a per-IP DB-syntax-error rate threshold.
  5. Deploy the database-process-spawns-a-shell rule — the highest-fidelity signal.
  6. Add the Suricata UNION SELECT rule between tiers where you have TLS visibility.
  7. Alert on verb anomalies (DROP/xp_cmdshell from a web account) and 50× row-count spikes.
  8. Patch database-engine and driver CVEs (e.g. CVE-2025-1094) on the app-CVE cadence.
  9. Fire every rule in a lab against true-positive and benign traffic; record false positives.

What does the database itself tell you?

Detection almost always sits at the application or WAF layer, which means it sees the request and not the consequence. The database has the other half of the story, and it is frequently the clearer half.

Three database-side signals are worth collecting. Query errors by type — a rise in syntax errors is one of the most reliable indicators that somebody is probing, because successful injection is preceded by a great deal of unsuccessful injection. Rows returned per query shape, since a query that normally returns one row and suddenly returns forty thousand is an exfiltration signature no request-side rule will catch. And queries from unexpected sources, which catches the case where an attacker has moved past the application entirely.

The first of those is the highest value and the least collected. Syntax errors are noise to a developer and evidence to a defender — route them somewhere a defender actually looks.

The takeaway

You cannot prove the absence of injection, so you instrument for its presence. Web signatures catch the loud attempts, behavioral analytics catch the blind ones, and a database process spawning a shell catches the one that already won. Parameterize to remove the bug class; detect to survive the regression. The same detect-and-defend arc carries into its sibling injection class, command injection logs: what to watch, and into file upload security, SSRF detection, and XSS CSP hardening — or browse the full Detection Engineering pillar.

Training & tools referenced

Disclosure: Some links below are affiliate links. If you buy through them, darkpwn may earn a commission at no extra cost to you. We only recommend training and tools we actually use in our own lab, and affiliate links never influence editorial coverage.

  • TryHackMeAuthorized labs to practice these SQLi detections against real telemetrySecurity Training
    Start training

Frequently asked questions

How do you detect SQL injection in logs?

Watch for a burst of database syntax errors ("unclosed quotation," "unterminated quoted string") from one source IP, requests carrying UNION SELECT or OR 1=1, and round-number response latencies that signal time-based blind injection. The highest-fidelity signal is a database process spawning a shell.

Can a WAF stop all SQL injection?

No. A WAF blocks obvious payloads at the edge and is worth running as defense in depth, but blind, second-order, and encoding-based variants slip past tuned-down rules. CVE-2025-1094 is the proof: the payload was valid escaped output, so a content-matching WAF had nothing to match. Parameterized queries are the real fix.

What is the difference between error-based and blind SQL injection?

Error-based injection leaks data through verbose database errors and is loud in logs. Blind injection reads answers indirectly, from page differences (boolean) or response delays (time-based), and produces no errors. Blind variants need behavioral and timing detection, not signatures.

Does using an ORM prevent SQL injection?

Mostly yes, when used normally — an ORM parameterizes queries for you and closes the class by default. The risk lives in the escape hatches: raw query methods, string-interpolated WHERE fragments, and dynamic ORDER BY clauses that most drivers cannot parameterize. Grep your codebase for the raw-query method names; that short list is your realistic injection surface.

How do you detect SQL injection at the database tier?

Two signals dominate. First, an application database account executing a verb it has never used before — application accounts issue a small, stable set of query shapes, so the first DROP, GRANT, or EXEC is a categorical anomaly rather than a threshold judgement. Second, a database engine process spawning a shell, which is post-exploitation and should be treated as an incident rather than an alert.

Which MITRE ATT&CK technique covers SQL injection?

SQL injection maps to T1190 (Exploit Public-Facing Application). When it pivots to code execution through database features like xp_cmdshell or psql meta-commands, add T1059 (Command and Scripting Interpreter).