Introduction

Every web application that stores data eventually runs a database query shaped by what the user typed. A login form asks for a username and a password; the app builds a SQL query from those values and asks the database whether they match a record.

That sounds harmless — until you realise the user controls part of the query text. If the app simply pastes the input into the SQL string, a clever user can close the string early and append their own SQL. The database has no idea the extra code was not written by the programmer; it obeys it faithfully.

SQL injection is the name for that trick. It has been ranked the number-one web vulnerability for over a decade, responsible for breaches at companies large and small. The root cause is not a complicated flaw — it is the failure to keep data separate from code.

Try It

The demo below simulates a login form backed by a tiny in-memory "database". The server builds its query in one of two modes — vulnerable (string concatenation) or safe (parameterized query). Try both and observe how the constructed query changes.

<!-- {{c_html_desc}} -->
<div class="panel">
  <h3 class="panel-title">{{title_login}}</h3>
  <div class="mode-row">
    <span class="mode-label">{{label_mode}}</span>
    <button id="btn-vuln" class="mode-btn active" type="button">{{btn_vulnerable}}</button>
    <button id="btn-safe" class="mode-btn" type="button">{{btn_safe}}</button>
  </div>
  <label class="field-label" for="inp-user">{{label_username}}</label>
  <input id="inp-user" type="text" class="inp" placeholder="{{ph_user}}" autocomplete="off" />
  <label class="field-label" for="inp-pass">{{label_password}}</label>
  <input id="inp-pass" type="password" class="inp" placeholder="{{ph_pass}}" autocomplete="off" />
  <button id="btn-login" type="button" class="btn-login">{{btn_login}}</button>
  <div class="sql-box">
    <span class="sql-label">{{label_query}}</span>
    <pre id="sql-preview" class="sql-pre"></pre>
  </div>
  <div id="result" class="result"></div>
</div>
/* {{c_css_desc}} */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; background: transparent; }
.panel { max-width: 480px; margin: 0 auto; padding: 1rem; }
.panel-title { margin: 0 0 .8rem; font-size: 1.1rem; font-weight: 700; }
.mode-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .9rem; flex-wrap: wrap; }
.mode-label { font-size: .85rem; color: #555; flex-shrink: 0; }
.mode-btn { font-size: .82rem; font-weight: 600; padding: .25rem .7rem; border-radius: 6px;
            border: 1.5px solid #aaa; background: #f0f2f4; color: #444; cursor: pointer; }
.mode-btn.active.vuln { border-color: #c92f3c; background: #fde8ea; color: #a01c27; }
.mode-btn.active.safe { border-color: #0a7d33; background: #d8f5e3; color: #065c26; }
.field-label { display: block; font-size: .82rem; color: #555; margin: .5rem 0 .2rem; }
.inp { width: 100%; padding: .45rem .6rem; font-size: .95rem; border: 1.5px solid #ccc;
       border-radius: 6px; outline: none; }
.inp:focus { border-color: #1d3557; }
.btn-login { margin-top: .75rem; width: 100%; padding: .5rem; font: 600 .95rem system-ui, sans-serif;
             background: #1d3557; color: #fff; border: none; border-radius: 8px; cursor: pointer; }
.btn-login:hover { background: #16294a; }
.sql-box { margin-top: .9rem; }
.sql-label { font-size: .78rem; font-weight: 600; color: #666; text-transform: uppercase; letter-spacing: .04em; }
.sql-pre { margin: .3rem 0 0; padding: .55rem .7rem; background: #f4f6f8; border: 1px solid #dde1e6;
           border-radius: 6px; font: 400 .8rem/1.5 ui-monospace, monospace; white-space: pre-wrap;
           word-break: break-all; color: #333; }
.sql-pre .inject { color: #c92f3c; font-weight: 700; }
.result { margin-top: .8rem; min-height: 2em; font-size: .95rem; font-weight: 600; padding: .45rem .7rem;
          border-radius: 6px; display: none; }
.result.ok { display: block; background: #d8f5e3; color: #065c26; border: 1px solid #a3d9b5; }
.result.fail { display: block; background: #fde8ea; color: #a01c27; border: 1px solid #f2b3b8; }
// Code not found

In vulnerable mode, typing ' OR '1'='1 as the username produces a query that is always true — bypassing the password check entirely. In safe mode the same input is treated as a literal string, so the query finds no matching user and login fails. The fix is not to sanitize input — it is to never build queries by concatenating strings.

The Real Complexity

SQL injection is not one trick — it is a family of attacks that all exploit the same root cause.

  • Classic injection: the attacker can read the database's response directly, as in the login-bypass demo. Type ' OR '1'='1 and the WHERE clause is always true.
  • Union-based extraction: appending UNION SELECT ... lets the attacker retrieve arbitrary tables, including password hashes, emails, and credit-card numbers.
  • Blind injection: the app doesn't echo the result, but the attacker can still extract data one bit at a time by asking yes/no questions — "is the first character of the admin password greater than 'm'?" — and observing whether the page changes.
  • Time-based blind: if the page looks identical either way, the attacker can use SLEEP(5) or WAITFOR DELAY to encode answers as response delays.
  • Second-order injection: malicious input is stored safely today but concatenated into a query later, when the app "trusts" its own database.

The unifying cause is a missing type boundary between code and data. In a properly parameterised query the database receives the SQL template and the user values as separate arguments. The query planner compiles the template first, before it ever sees the user's text — so a quote in the input is just a character in a string, not SQL syntax.

Related ideas: pattern matching studies how structure is found in strings; program synthesis explores building correct programs from specifications — both fields care deeply about the boundary between structure and content.

Where It Matters

SQL injection is not an academic toy — it has caused some of the largest data breaches in history. The lesson applies wherever user input reaches a database:

  • Web applications: login forms, search boxes, URL parameters, and HTTP headers are all injection surfaces. OWASP has listed injection as the top web risk for over a decade.
  • APIs and mobile backends: REST endpoints that build queries from JSON fields are just as vulnerable as HTML forms.
  • Admin panels: internal tools that "trust" employees can still be hit by injected payloads in data imported from external sources (second-order attacks).
  • Stored procedures: calling a stored procedure with concatenated arguments doesn't protect you — the injection just moves one layer deeper.

The fix is always the same: use parameterized queries (also called prepared statements) in every database call. Most frameworks and ORMs do this by default — the danger comes from raw string-building, often introduced by developers who think they can sanitize their way out of the problem.

Complementary defences add depth: a least-privilege database account limits what an attacker can do even after a successful injection; input validation catches obvious garbage early; and WAFs (web application firewalls) can block known patterns. But none of these replace parameterized queries.

Conclusion

SQL injection is, in a technical sense, a solved problem. Parameterized queries separate the query template from the data at the type level — the database engine never sees a user-supplied string as SQL syntax. The attack surface vanishes.

What keeps SQL injection alive is not a lack of knowledge but a lack of discipline. String concatenation is easy to write and easy to miss in a code review. ORMs hide the danger until a developer reaches for raw SQL. Legacy code lingers.

The deeper lesson is architectural: data and code must never share the same channel without an explicit type boundary. That principle echoes through pattern matching, compiler design, and every system where untrusted input meets a structured interpreter. Get the boundary right and an entire class of attacks disappears.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/sql-injection/Content licensed under CC BY-NC 4.0.