Repository navigation
Security
What quill protects you from, what it cannot, and what to do about the rest. Each section names its risk area.
On this page: Injection · Raw SQL · Dynamic identifiers · Mass writes · Logging and personal data · Cursors · Resource handling · Checklist
Risk area: input validation.
| Input | Protection |
|---|---|
Values (Where, Values, Set, ...) |
Always sent as bind arguments, never put in the SQL text |
| Identifiers (tables, columns, aliases) | Always quoted. An embedded quote character is doubled, so a name cannot close its own quotes |
| Operators | Allow-listed. Anything else is db.ErrInvalidOperator
|
| Order direction | Only asc or desc. Anything else is db.ErrInvalidDirection
|
| JSON paths | Single quotes are escaped, so a path stays inside its SQL literal |
| Index names | Validated against [a-zA-Z0-9_$] because they are emitted unquoted |
WhereIntegerInRaw |
Only integers accepted. Anything else is db.ErrInvalidArgs
|
Quoting makes an identifier safe to embed, not safe to choose. See dynamic identifiers.
Risk area: injection through trusted-text APIs.
These methods insert SQL text exactly as written. Never build their text from user input.
db.Raw, SelectRaw, WhereRaw, OrWhereRaw, HavingRaw, OrHavingRaw, GroupByRaw, OrderByRaw, FromRaw, Lock, the join clause's WhereRaw / OrWhereRaw, and DB.Query / DB.Exec.
q := conn.Select().From("users")
// WRONG: user input becomes SQL
q.WhereRaw("name = '" + name + "'")
// RIGHT: the value is an argument
q.WhereRaw("lower(name) = ?", strings.ToLower(name))
q.Where("name", name)? in raw text becomes a bind marker ($n on PostgreSQL). Markers inside quoted strings or quoted identifiers are left alone, and ?? is a literal ?.
Risk area: access control.
If a column, table or sort field comes from a request (?sort=created_at), quoting prevents SQL injection, but a caller could still pick a column you did not mean to expose, such as password_hash. Allow-list the names:
sortable := map[string]string{"created": "created_at", "name": "name"}
col, ok := sortable[r.URL.Query().Get("sort")]
if !ok {
col = "created_at"
}
q.OrderBy(col, dir) // dir is validated to asc/desc by the libraryThe same applies to column names passed to Where, Select, Pluck, the cursor column, and table names passed to db.As or From.
Risk area: data integrity.
An unbounded UPDATE or DELETE is refused with db.ErrNoWhere unless the query has a WHERE or LIMIT, or you call AllRows() to say you mean every row. conn.Truncate(ctx, table) is the explicit way to empty a table, and on PostgreSQL it does not cascade to referencing tables. Details.
DB.UpdateOrInsert is not atomic. If concurrent writers could create duplicates, use a transaction and a unique index, or Upsert.
Risk area: data exposure.
Bind values routinely hold personal data (names, emails, identity numbers, card or account numbers).
-
db.WithHookevents carry the SQL (with placeholders) and the argument count, not the values. -
db.WithLogArgs()adds the values toQueryEvent.Args. Enable it only where that data may be logged, stored and access-controlled accordingly. -
ToRawSQL()andMustRawSQL()(select builder only) interpolate values into the text. They exist for debugging only. Never execute or log their output in production, because it is not injection-safe and it contains the data. - Driver and database errors can echo values back. Treat
QueryEvent.Erras sensitive too. - Keep database credentials (the DSN) out of source code and logs. Load them from a secret store. If one leaks, rotate it.
Risk area: untrusted input.
A keyset cursor (CursorPaginate) is a token that travels through clients, so it is parsed as untrusted input. It decodes to a plain string, boolean or number that is only bound as an argument. A forged token can move the page position or be rejected with ErrInvalidArgs, but it cannot alter the SQL. The cursor column comes from your code, never from the token.
The token is not encrypted or signed. It reveals the last row's key value, so do not use it for anything secret. If positions must not be forgeable, sign the token in your own layer.
Risk area: availability.
- Rows are always closed, including when your callback returns an error.
- Every method honours the
context, so a cancelled request stops its queries. -
StreamandEachhold one row in memory. Breaking out of aStreamloop closes the cursor. - Transactions roll back on an error and on a panic, then re-raise the panic.
-
TableName()runs behind arecover, so a faulty method returns an error instead of crashing the process.
- Values go in as arguments, never into raw text.
- Names that come from input are allow-listed.
-
*Rawmethods only ever receive literals you wrote. - Hooks do not log arguments unless that is intended and controlled.
-
ToRawSQLis not used outside local debugging. - Cursor columns and sort fields are fixed by your code.
- Multi-statement changes that must stay consistent run in a
Transaction.
quill wiki · v0.2.0 · Repository · pkg.go.dev · Report an issue · MIT License
Basics
Building queries
Data access
- Writing Data
- Reading Data
- Pagination and Chunking
- Transactions
- Typed API
- Struct Mapping
- Hooks and Options
Reference
Guides