SQL Injection in Pull Requests: How to Detect Vulnerabilities Before Merge

This article covers what SQL injection actually looks like in a pull request, why reviewers miss it, and how automated scanning catches it before the merge button gets clicked.

12 min read
GitHubGitHub Pull Request SecuritySQL injection
Featured image for SQL Injection in Pull Requests: How to Detect Vulnerabilities Before Merge

A single unparameterised query string is still one of the most reliable ways to compromise an application. Not because developers don't know SQL injection exists — most learned about it in their first year of writing backend code — but because the vulnerable pattern is easy to write by accident and easy to miss during review. It hides in a helper function, a report generator, an admin tool nobody looks at twice. Then it ships.

This article covers what SQL injection actually looks like in a pull request, why reviewers miss it, and how automated scanning catches it before the merge button gets clicked.

What Is SQL Injection?

SQL injection happens when untrusted input gets concatenated directly into a SQL query instead of being passed as a parameter. The database can't tell the difference between the query the developer intended and the query an attacker just appended to it.

SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1'

That second clause always evaluates to true, so the query returns every row in the users table regardless of the password submitted. This is the textbook example, but production SQL injection is rarely that clean — it's usually buried in a WHERE clause built from three or four string concatenations across a service layer.

The impact ranges from data exfiltration to full authentication bypass, and in some database configurations, remote code execution via stacked queries or xp_cmdshell. It's been on the OWASP Top 10 since the list existed for a reason: the fix is well understood, but the mistake keeps getting made.

Why SQL Injection Still Happens

A few recurring reasons show up across most codebases:

  • ORMs get bypassed for "one quick query." A developer needs a dynamic filter or a report with configurable columns, decides the ORM's query builder is too limiting, and drops down to raw SQL with string interpolation.
  • Legacy code gets copied forward. Old patterns from a codebase written before parameterised queries were standard practice get pasted into new files because "that's how we do it here."
  • Input validation is treated as a substitute for parameterisation. Sanitising input helps, but it's not a replacement for binding parameters at the query layer. Blocklists miss encoding tricks and edge cases.
  • Internal tools get a pass. Admin dashboards and internal scripts often skip the security scrutiny given to public-facing endpoints, even though they frequently have direct database access and elevated privileges.

None of this is exotic. It's the accumulated result of deadlines, unfamiliarity with a particular ORM's escape hatches, and the assumption that "internal" means "safe."

How SQL Injection Slips Through Pull Requests

Most SQL injection doesn't get introduced in a 500-line feature PR that everyone scrutinises carefully. It shows up in:

  • Small PRs that look routine — a new filter parameter, a search endpoint, a bulk export feature.
  • Refactors where a parameterised query gets rewritten as a string template "for readability" and the binding gets dropped in the process.
  • Utility functions imported into multiple places, where the vulnerability exists once but the blast radius is every caller.
  • Dynamic table or column names, which can't be parameterised the normal way and require allowlisting — a step that's easy to skip under time pressure.

The common thread is that these changes don't look dangerous on the surface. A three-line diff adding a WHERE clause reads as low-risk to a reviewer who's mentally triaging a stack of PRs before lunch.

Real Examples of SQL Injection in Code Reviews

Here's a pattern that comes up constantly in review: a developer adds sorting to an API endpoint.

// Vulnerable
app.get('/products', (req, res) => {
  const sortColumn = req.query.sort || 'name';
  const query = `SELECT * FROM products ORDER BY ${sortColumn}`;
  db.query(query, (err, results) => res.json(results));
});

This passes review because the reviewer is thinking about the sorting logic, not the fact that sort is user-controlled and gets dropped straight into the query string. An attacker sends ?sort=name;DROP TABLE products;-- and the query executes whatever follows.

Another common one: building a search filter across optional fields.

# Vulnerable
def search_users(name=None, email=None):
    query = "SELECT * FROM users WHERE 1=1"
    if name:
        query += f" AND name = '{name}'"
    if email:
        query += f" AND email = '{email}'"
    return db.execute(query)

Each conditional looks harmless individually. Reviewed together, in a PR with five other conditionals just like it, the pattern rarely gets flagged unless someone is specifically looking for string-built queries.

Common SQL Injection Patterns

JavaScript (vulnerable)

const query = `SELECT * FROM orders WHERE customer_id = ${customerId}`;
db.query(query);

JavaScript (secure)

const query = 'SELECT * FROM orders WHERE customer_id = ?';
db.query(query, [customerId]);

TypeScript (vulnerable)

async function getOrder(orderId: string) {
  return await db.query(`SELECT * FROM orders WHERE id = '${orderId}'`);
}

TypeScript (secure)

async function getOrder(orderId: string) {
  return await db.query('SELECT * FROM orders WHERE id = $1', [orderId]);
}

Python (vulnerable)

cursor.execute(f"SELECT * FROM invoices WHERE user_id = {user_id}")

Python (secure)

cursor.execute("SELECT * FROM invoices WHERE user_id = %s", (user_id,))

PHP (vulnerable)

$query = "SELECT * FROM accounts WHERE username = '" . $_GET['username'] . "'";
$result = mysqli_query($conn, $query);

PHP (secure)

$stmt = $conn->prepare("SELECT * FROM accounts WHERE username = ?");
$stmt->bind_param("s", $_GET['username']);
$stmt->execute();

Why this works: parameterised queries send the query structure and the user-supplied values to the database separately. The database engine treats bound values strictly as data, never as executable SQL, regardless of what characters they contain. String concatenation, by contrast, hands the database a single blob of text with no way to distinguish code from data.

Why Manual Code Review Misses SQL Injection

Human reviewers are good at catching logic errors, naming issues, and architectural concerns. They're less consistent at catching security flaws, and there are specific reasons for that:

  • Reviewers optimise for the diff, not the data flow. A three-line change gets read as three lines, not traced back to where the input originates and how it's validated upstream.
  • Context gets lost across files. The vulnerable string concatenation might be in a utility file that wasn't touched in this PR at all — only the caller was added.
  • Review fatigue is real. By the tenth PR of the day, pattern recognition drops. Security issues that would be obvious in isolation get skimmed past.
  • Not every reviewer has a security background. Most engineers reviewing PRs are checking for correctness and maintainability. Security-specific review requires a different mental model, and not every team has someone dedicated to applying it consistently.

This isn't a criticism of reviewers — it's a description of what manual review is actually good at, and injection flaws sit outside that strength.

How Automated Pull Request Security Scanning Detects SQL Injection

Static analysis tools built for pull request scanning work by tracing data flow: they follow user-controlled input (query parameters, form fields, headers, request bodies) from its entry point through the application to see where it ends up. If that input reaches a database call without passing through a parameterisation layer, that's flagged.

This is different from generic pattern-matching that just greps for string concatenation near the word SELECT. Real taint analysis understands whether a variable is actually user-controlled versus a static value, which cuts down on false positives that would otherwise train a team to ignore the scanner.

Good pull request scanning also runs at the point the PR is opened, not on a nightly schedule against the whole repository. That timing matters — a finding surfaced while the diff is still fresh in the developer's head gets fixed in minutes. The same finding surfaced two weeks later in a scheduled scan against main requires someone to reconstruct context, and by then it may already be in production.

This is the same principle covered in more depth in GitHub Pull Request Security: Complete Guide (2026) — catching vulnerability classes at the PR stage rather than after merge is what shift-left security actually means in practice, as opposed to the phrase alone.

How Launchioo Detects SQL Injection Before Merge

Launchioo scans GitHub pull requests directly, looking for SQL injection alongside other classes of vulnerability that tend to slip through manual review: SSRF, CSRF, hardcoded secrets, other injection flaws, exploit chains that combine multiple lower-severity issues into something exploitable, and supply chain risks introduced through dependency changes.

For SQL injection specifically, it traces how request data moves through the diff and flags places where that data reaches a query without going through parameter binding. Findings appear as comments directly on the relevant lines of the pull request, with an explanation of the data flow and a suggested fix, so the developer can address it without leaving GitHub or waiting for a separate security review cycle.

If a PR also introduces a hardcoded credential or a request to an unvalidated internal URL, those get flagged in the same pass — see How to Detect Hardcoded Secrets in GitHub Pull Requests (Before They Reach Production) and [What Is SSRF? How to Detect Server-Side Request Forgery in GitHub Pull Requests]https://www.launchioo.com/blog/what-is-ssrf-how-to-detect-server-side-request-forgery-in-github-pull-requests) for how those specific checks work. CSRF gets covered separately in What Is CSRF? Detecting CSRF Vulnerabilities in Pull Requests.

Setup is a GitHub App install, connected at launchioo.com/install. Once it's authorised against a repository, it scans new pull requests automatically. Existing users can review scan history and configuration from launchioo.com/login.

None of this replaces code review — it's a check that runs alongside it, catching the class of bug that's specifically hard for a human reading a diff to spot reliably.

Best Practices for Preventing SQL Injection

  • Use parameterised queries or an ORM's query builder by default. Reach for raw SQL only when there's a specific reason to, and treat that as a decision worth flagging in review, not a default.
  • Allowlist dynamic identifiers. If a column or table name needs to be dynamic, validate it against a fixed list of known-safe values rather than trusting it directly — identifiers can't be parameterised the same way values can.
  • Apply least privilege at the database user level. A compromised query should not have permission to drop tables or read from schemas it has no business touching.
  • Add SQL injection checks to CI, not just to human review. Automated scanning catches what fatigue and context-switching cause reviewers to miss.
  • Treat internal tools with the same scrutiny as public endpoints. Internal dashboards are a common blind spot precisely because they're assumed to be low-risk.
  • Log and monitor for anomalous query patterns. Detection at runtime is a backstop, not a substitute for fixing the code, but it shortens the window between introduction and discovery if something does slip through.

Frequently Asked Questions

Is SQL injection still a real threat in 2026? Yes. It remains one of the most commonly exploited vulnerability classes in production applications, particularly in older codebases and internal tools that see less security scrutiny than public-facing services.

Can an ORM completely prevent SQL injection? An ORM significantly reduces risk when used as intended, but most ORMs allow raw SQL or raw query fragments for cases the query builder can't express. Injection can still occur wherever raw SQL is used with concatenated input.

Does input validation stop SQL injection? It helps but isn't sufficient on its own. Parameterised queries are the primary defence; validation is a secondary layer that reduces the attack surface for other issues too.

Why do automated scanners catch SQL injection that reviewers miss? Scanners trace data flow programmatically across the entire codebase, including files not touched in the current PR, and don't suffer from review fatigue or limited context windows the way a human skimming a diff does.

Can stored procedures prevent SQL injection? Only if they're called with parameters rather than having dynamic SQL built inside them. A stored procedure that concatenates input internally is just as vulnerable as inline SQL.

Is NoSQL immune to injection attacks? No. NoSQL databases have their own injection variants — MongoDB operator injection is a well-known example — where user input manipulates query operators instead of SQL syntax.

What's the difference between blind and in-band SQL injection? In-band injection returns data directly in the application's response. Blind injection doesn't return visible output, so an attacker infers results through timing differences or boolean true/false behaviour instead.

Should PR scanning replace manual code review for security? No — the two catch different things. Automated scanning is consistent and fast at pattern and data-flow detection; manual review is better at catching business logic flaws and architectural risk that a scanner has no context to evaluate.

How quickly should a flagged SQL injection finding be fixed? Before merge, ideally. Fixing it while the PR is open and the developer has full context takes minutes. Fixing the same issue after it's shipped to production requires reconstructing that context and carries actual exposure risk in the meantime.

Does parameterisation affect query performance? Not meaningfully, and in most databases it improves performance slightly, since parameterised queries allow the database to cache and reuse query execution plans.

SQL injection isn't a hard vulnerability to fix once it's found — the fix is usually a few lines. The difficulty is finding it before it ships, in a diff that looks routine, reviewed by someone triaging a dozen other PRs the same afternoon. Catching it at the pull request stage, before merge, is what actually moves the cost of a fix from "incident response" to "one comment on a diff."

Try Launchioo on your repositories

Install the GitHub App and get automated pull request security reviews in minutes.

Security FAQ

What is SSRF?
Server-Side Request Forgery (SSRF) is a vulnerability where an application makes HTTP or other network requests to a destination controlled or influenced by an attacker, often leading to internal network access or data exposure.
How do I detect SSRF in a pull request?
Review added and changed lines for outbound requests that use user-controlled URLs, hosts, or paths. Automated PR scanners like Launchioo flag common SSRF patterns in diffs before merge.
Can automated scanners catch SSRF before merge?
Yes. Deterministic rules can detect many SSRF patterns in pull request diffs — including user-controlled fetch targets and unsafe URL construction — when scans run on each PR update.
Why is GitHub pull request scanning important?
Pull request scanning checks only changed code before it reaches the default branch, catching secrets, SSRF, injection, and supply-chain issues at the point where fixes are cheapest.

See the product documentation and pricing for plan details.

Related articles