GORM's default logger prints the full interpolated SQL, including the client-chosen path and username, on every record-not-found #178
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found by the log-call audit required by #176. Filed rather than fixed there: that issue scopes its sweep to
slogcall sites, and this is a second, independent log writer whose configuration is an observability decision in its own right.internal/database/database.go:156andinternal/database/webhook_db_manager.go:249both open GORM with a bare&gorm.Config{}. That leaveslogger.Defaultin place, which ingorm.io/gorm v1.25.5is:logger.Tracelogs whenevererr != nil && LogLevel >= Error && (!errors.Is(err, ErrRecordNotFound) || !IgnoreRecordNotFoundError).Warn(3) is>= Error(2) andIgnoreRecordNotFoundErroris false, so everyErrRecordNotFoundprints the fully interpolated SQL toos.Stdout, unconditionally.Two of those lookups are on unauthenticated endpoints and miss by design:
internal/handlers/webhook.golookupEntrypoint—SELECT * FROM entrypoints WHERE path = "<the client's path segment>", on/webhook/{uuid}, which matches any single segment of any length.internal/handlers/auth.goauthenticateUser—SELECT * FROM users WHERE username = "<the submitted username>", on the login form.Observed directly, in the test run inside the CI image:
Those were the 8 KB client-chosen values the new bound tests send. The
sloglines for the same two requests are capped at 512 encoded bytes; these are not capped at all.Why this is worse than the one that disclosed it
#176 and #146 both concerned lines an operator can reason about: they go through the service's own logger, they carry a level, and the two
DEBUGones are off by default. This one is on by default, answers to no level the operator sets, does not go throughinternal/logger(so neither the JSON nor the tty handler shapes it, and it lands on stdout rather than stderr), and is not covered by theMaxAccessLogLineBytesceiling the README now quotes.Net: an unauthenticated client on the public internet still writes arbitrary-length attacker-chosen text into the operator's logs, one line per request, at the full 1 MB the receiver will accept as a path. That is the same defect class #146 was moved into the
1.0.0milestone for, which is why it is milestoned the same way.The decision to make
Not simply "cap it" — GORM's logger is an interface, and the choice of what to install has consequences beyond this path:
IgnoreRecordNotFoundError: true. Kills these two lines outright. A missing row is not an error on either of these paths; it is the expected outcome for an invented UUID or an unknown user. Cheapest fix, and it removes the amplification rather than bounding it.internal/loggerwith agormlogger.Interfaceadapter, so its output has a level, a handler and a destination consistent with everything else — and sointernal/logfieldcan cap what it emits. More work; the right end state if SQL logging is wanted at all.LogLevel: logger.Silentin production and keep the default in dev. Loses slow-query logging, which is the one genuinely useful thing this logger does.Whichever is chosen, the SQL text itself needs a bound if it is logged at all: GORM interpolates the parameters into the statement, so the line length is the parameter length, and on these two paths the parameter is client-chosen.
Definition of done
slog.internal/middleware/logbound_test.go, capturing GORM's writer rather than the service logger.Implementation requirements
next, PR based onnext, single commit, title ending(closes #N).TODO.md(see #112).make checkplus the Docker lint path with the cache defeated. All linting runs in Docker.