Overview
The CVE-2026-33503 vulnerability in Ory Kratos demonstrates how insecure pagination can enable SQL injection. Prior to version 26.2.0, the ListCourierMessages Admin API relied on pagination tokens encrypted with a secret configured in secrets.pagination. If this secret was not customized, Kratos fell back to a default, publicly known value, enabling attackers who know or guess the secret to craft pagination tokens that inject SQL into the backend. The real-world impact can include unauthorized data access, data modification, or even disruption of the Admin API, depending on the database permissions and the scope of the token’s effect. This reinforces the broader lesson that token-driven pagination must not be able to inject SQL fragments into queries and that secrets must be protected and unique to each deployment.
In exploitation terms, an attacker crafts a specially forged pagination token that, once decrypted, yields SQL fragments or controls that are concatenated into a query used by the Admin API. If the server builds SQL by string concatenation or fmt.Sprintf with decrypted token content instead of using parameterized queries, the injected fragment can alter the query structure, potentially leaking data or modifying records. The root cause is assuming decrypted token data is safe to splice into SQL, compounded by a weak or default pagination secret.
For Go (Gin) applications with similar pagination patterns, this vulnerability manifests when a decrypted token is used to form raw SQL. The safe approach is to deserialize token payload into a strongly typed structure (e.g., a Pager with numeric fields), validate those fields, and pass them as query parameters. Always configure secrets.pagination with a cryptographically secure random value, and upgrade to a fixed Kratos version (26.2.0+) if you rely on Kratos’ pagination logic. In your own Go code, avoid including any token-derived SQL fragments in queries and instead rely on parameterized statements and strict input validation.
Code Fix Example
Go (Gin) API Security Remediation
// Vulnerable pattern and safe fix in Go (Gin-like pseudocode)
package main
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
_ "github.com/mattn/go-sqlite3"
)
type Pager struct {
Offset int `json:"offset"`
Limit int `json:"limit"`
}
// decryptToken is a placeholder for real decryption using secrets.pagination in production.
func decryptToken(token string) string {
// In a real system, this would decrypt the token. For this example, we return the token as-is.
return token
}
// Vulnerable: token content is concatenated into SQL
func vulnerableList(db *sql.DB, token string) ([]string, error) {
decoded := decryptToken(token) // attacker-controlled after decryption
// Vulnerable: direct string concatenation of decrypted content into SQL
query := "SELECT id, message FROM courier_messages WHERE archived = 0 ORDER BY id " + decoded
rows, err := db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var res []string
for rows.Next() {
var id int
var msg string
if err := rows.Scan(&id, &msg); err != nil {
break
}
res = append(res, fmt.Sprintf("%d:%s", id, msg))
}
return res, nil
}
// Safe: decode to structured data and use parameterized queries
func safeList(db *sql.DB, token string) ([]string, error) {
payload := decryptToken(token)
var pager Pager
if err := json.Unmarshal([]byte(payload), &pager); err != nil {
return nil, err
}
if pager.Limit <= 0 || pager.Offset < 0 {
return nil, errors.New("invalid pagination values")
}
// Use parameterized query to prevent injection
rows, err := db.Query("SELECT id, message FROM courier_messages WHERE archived = 0 ORDER BY id LIMIT ? OFFSET ?", pager.Limit, pager.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var res []string
for rows.Next() {
var id int
var msg string
if err := rows.Scan(&id, &msg); err != nil {
break
}
res = append(res, fmt.Sprintf("%d:%s", id, msg))
}
return res, nil
}
func main() {
// This is a minimal placeholder to illustrate the fix; in production, wire this into a Gin route.
db, _ := sql.Open("sqlite3", ":memory:")
defer db.Close()
db.Exec("CREATE TABLE courier_messages (id INTEGER PRIMARY KEY, message TEXT, archived INTEGER)")
db.Exec("INSERT INTO courier_messages (message, archived) VALUES ('hello', 0), ('world', 0)")
// Example usage would call vulnerableList(db, token) or safeList(db, token)
}