SQL injection: authentication bypass with a single quote
User input concatenated into a query becomes part of the SQL.
When input is concatenated into a SQL string, it can change the structure of the query, not just its values.
SQL injection live demo
SELECT * FROM users WHERE name = 'alice' AND pass_hash = '' Your input lands inside the query. Try a quote.
Submit the form.
The bug
q := "SELECT * FROM users WHERE name = '" + name + "' AND pass_hash = '" + hash + "'" With name set to admin' --:
SELECT * FROM users WHERE name = 'admin' --' AND pass_hash = '…' The quote closes the string literal, and -- turns the password check into a comment.
Beyond login
- UNION-based extraction of other tables.
- Blind injection (boolean or time-based) when nothing is reflected in the response.
- Stacked queries, where the driver allows several statements per call.
- File access or command execution, depending on the DBMS and the account’s privileges:
xp_cmdshellon SQL Server,COPY ... PROGRAMfor a PostgreSQL superuser.
Where it still appears
ORMs and query builders cover the common path. What remains:
- Identifiers (
ORDER BYcolumns, table names), which cannot be bound as parameters. - Raw SQL escape hatches in ORMs.
- Search and filter builders, report generators.
- Second-order injection: a value stored safely, then concatenated into a later query.
Fix
Bind parameters, so the query structure is fixed before the data arrives:
var id int64
var hash []byte
err := db.QueryRowContext(ctx,
"SELECT id, pass_hash FROM users WHERE name = $1", name,
).Scan(&id, &hash) Then verify the password in code with bcrypt or Argon2id, not in SQL.
Identifiers come from an allow-list:
columns := map[string]string{"date": "created_at", "total": "amount"}
col, ok := columns[r.URL.Query().Get("sort")]
if !ok {
col = "created_at"
} Run the application with a database role that has only the privileges it needs.
I test web apps, APIs and LLM integrations. Contact.
Next article → IDOR: reading other users’ invoices by changing an ID