diff --git a/go/ql/lib/change-notes/2026-06-17-model-log-slog.md b/go/ql/lib/change-notes/2026-06-17-model-log-slog.md index 06bba53a6ed1..5db7a8c47248 100644 --- a/go/ql/lib/change-notes/2026-06-17-model-log-slog.md +++ b/go/ql/lib/change-notes/2026-06-17-model-log-slog.md @@ -1,8 +1,4 @@ --- category: minorAnalysis --- -* Added models for the `log/slog` package (Go 1.21+). Its logging functions and - `*slog.Logger` methods (`Debug`/`Info`/`Warn`/`Error`, their `Context` - variants, and `Log`/`LogAttrs`) are now recognized as logging sinks, so the - `go/log-injection` and `go/clear-text-logging` queries cover code that logs - through `slog`. + * Added models for the `log/slog` package (Go 1.21+), including `*slog.Logger` methods, `With`/`WithGroup`, and `Attr`/`Value` helpers, improving coverage for the `go/log-injection` and `go/clear-text-logging` queries. diff --git a/go/ql/lib/ext/log.slog.model.yml b/go/ql/lib/ext/log.slog.model.yml index 3283492c226e..888f2b54aaec 100644 --- a/go/ql/lib/ext/log.slog.model.yml +++ b/go/ql/lib/ext/log.slog.model.yml @@ -27,3 +27,27 @@ extensions: - ["log/slog", "Logger", True, "ErrorContext", "", "", "Argument[1..2]", "log-injection", "manual"] - ["log/slog", "Logger", True, "Log", "", "", "Argument[2..3]", "log-injection", "manual"] - ["log/slog", "Logger", True, "LogAttrs", "", "", "Argument[2..3]", "log-injection", "manual"] + # With/WithGroup add attributes that are included in every subsequent log call. + - ["log/slog", "", False, "With", "", "", "Argument[0]", "log-injection", "manual"] + - ["log/slog", "Logger", True, "With", "", "", "Argument[0]", "log-injection", "manual"] + - ["log/slog", "Logger", True, "WithGroup", "", "", "Argument[0]", "log-injection", "manual"] + - addsTo: + pack: codeql/go-all + extensible: summaryModel + data: + # Constructors for Attr that can carry a tainted string into the result. + - ["log/slog", "", False, "Any", "", "", "Argument[0..1]", "ReturnValue", "taint", "manual"] + - ["log/slog", "", False, "Group", "", "", "Argument[0]", "ReturnValue", "taint", "manual"] + - ["log/slog", "", False, "Group", "", "", "Argument[1].ArrayElement", "ReturnValue", "taint", "manual"] + - ["log/slog", "", False, "GroupAttrs", "", "", "Argument[0]", "ReturnValue", "taint", "manual"] + - ["log/slog", "", False, "GroupAttrs", "", "", "Argument[1].ArrayElement", "ReturnValue", "taint", "manual"] + - ["log/slog", "", False, "String", "", "", "Argument[0..1]", "ReturnValue", "taint", "manual"] + # Constructors for Value that can carry a tainted string into the result. + - ["log/slog", "", False, "AnyValue", "", "", "Argument[0]", "ReturnValue", "taint", "manual"] + - ["log/slog", "", False, "GroupValue", "", "", "Argument[0].ArrayElement", "ReturnValue", "taint", "manual"] + - ["log/slog", "", False, "StringValue", "", "", "Argument[0]", "ReturnValue", "taint", "manual"] + # Methods that read a string back out of an Attr or Value. + - ["log/slog", "Attr", True, "String", "", "", "Argument[receiver]", "ReturnValue", "taint", "manual"] + - ["log/slog", "Value", True, "Any", "", "", "Argument[receiver]", "ReturnValue", "taint", "manual"] + - ["log/slog", "Value", True, "Group", "", "", "Argument[receiver]", "ReturnValue.ArrayElement", "taint", "manual"] + - ["log/slog", "Value", True, "String", "", "", "Argument[receiver]", "ReturnValue", "taint", "manual"] diff --git a/go/ql/test/library-tests/semmle/go/concepts/LoggerCall/slog.go b/go/ql/test/library-tests/semmle/go/concepts/LoggerCall/slog.go index 63bb0a817925..3e7e660822fb 100644 --- a/go/ql/test/library-tests/semmle/go/concepts/LoggerCall/slog.go +++ b/go/ql/test/library-tests/semmle/go/concepts/LoggerCall/slog.go @@ -37,4 +37,9 @@ func slogTest() { slog.InfoContext(ctx, text, key, v) // $ logger=text logger=key logger=v slog.Log(ctx, slog.LevelInfo, text, key, v) // $ logger=text logger=key logger=v slog.LogAttrs(ctx, slog.LevelInfo, text, attr) // $ logger=text logger=attr + + // With/WithGroup add attributes that are included in every subsequent log call. + logger.With(key, v) // $ logger=key logger=v + logger.WithGroup(text) // $ logger=text + slog.With(key, v) // $ logger=key logger=v } diff --git a/go/ql/test/library-tests/semmle/go/frameworks/Slog/CONSISTENCY/DataFlowConsistency.expected b/go/ql/test/library-tests/semmle/go/frameworks/Slog/CONSISTENCY/DataFlowConsistency.expected new file mode 100644 index 000000000000..8242ca2cdb72 --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/frameworks/Slog/CONSISTENCY/DataFlowConsistency.expected @@ -0,0 +1,2 @@ +reverseRead +| test.go:114:21:114:33 | call to Group | Origin of readStep is missing a PostUpdateNode. | diff --git a/go/ql/test/library-tests/semmle/go/frameworks/Slog/TaintFlows.expected b/go/ql/test/library-tests/semmle/go/frameworks/Slog/TaintFlows.expected new file mode 100644 index 000000000000..42831abaf155 --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/frameworks/Slog/TaintFlows.expected @@ -0,0 +1,2 @@ +invalidModelRow +testFailures diff --git a/go/ql/test/library-tests/semmle/go/frameworks/Slog/TaintFlows.ql b/go/ql/test/library-tests/semmle/go/frameworks/Slog/TaintFlows.ql new file mode 100644 index 000000000000..91b543f041c8 --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/frameworks/Slog/TaintFlows.ql @@ -0,0 +1,14 @@ +import go +import semmle.go.dataflow.ExternalFlow +import ModelValidation +import utils.test.InlineFlowTest + +module Config implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { + source.(DataFlow::CallNode).getTarget().getName() = ["getUntrustedData", "getUntrustedString"] + } + + predicate isSink(DataFlow::Node sink) { sink = any(LoggerCall log).getAMessageComponent() } +} + +import FlowTest diff --git a/go/ql/test/library-tests/semmle/go/frameworks/Slog/go.mod b/go/ql/test/library-tests/semmle/go/frameworks/Slog/go.mod new file mode 100644 index 000000000000..a81507537ff9 --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/frameworks/Slog/go.mod @@ -0,0 +1,3 @@ +module codeql-go-tests/frameworks/slog + +go 1.26 diff --git a/go/ql/test/library-tests/semmle/go/frameworks/Slog/test.go b/go/ql/test/library-tests/semmle/go/frameworks/Slog/test.go new file mode 100644 index 000000000000..cf41ec09994f --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/frameworks/Slog/test.go @@ -0,0 +1,115 @@ +package main + +import ( + "context" + "log/slog" +) + +func main() {} + +func getUntrustedData() interface{} { return nil } + +func getUntrustedString() string { + return "tainted string" +} + +// Package-level convenience functions. + +func testSlogDebug() { + slog.Debug(getUntrustedString()) // $ hasValueFlow="call to getUntrustedString" + slog.Debug("msg", "key", getUntrustedData()) // $ hasValueFlow="call to getUntrustedData" + slog.Debug("msg", slog.String("key", getUntrustedString())) // $ hasTaintFlow="call to String" +} + +func testSlogInfo() { + slog.Info(getUntrustedString()) // $ hasValueFlow="call to getUntrustedString" + slog.Info("msg", slog.Any("key", getUntrustedData())) // $ hasTaintFlow="call to Any" + slog.Info("msg", slog.String("key", getUntrustedString())) // $ hasTaintFlow="call to String" +} + +func testSlogWarn() { + slog.Warn(getUntrustedString()) // $ hasValueFlow="call to getUntrustedString" + slog.Warn("msg", slog.String("key", getUntrustedString())) // $ hasTaintFlow="call to String" +} + +func testSlogError() { + slog.Error(getUntrustedString()) // $ hasValueFlow="call to getUntrustedString" + slog.Error("msg", slog.String("key", getUntrustedString())) // $ hasTaintFlow="call to String" +} + +func testSlogContextVariants(ctx context.Context) { + slog.DebugContext(ctx, getUntrustedString()) // $ hasValueFlow="call to getUntrustedString" + slog.InfoContext(ctx, getUntrustedString()) // $ hasValueFlow="call to getUntrustedString" + slog.WarnContext(ctx, getUntrustedString()) // $ hasValueFlow="call to getUntrustedString" + slog.ErrorContext(ctx, getUntrustedString()) // $ hasValueFlow="call to getUntrustedString" + slog.InfoContext(ctx, "msg", slog.String("key", getUntrustedString())) // $ hasTaintFlow="call to String" +} + +func testSlogLog(ctx context.Context) { + slog.Log(ctx, slog.LevelInfo, getUntrustedString()) // $ hasValueFlow="call to getUntrustedString" + slog.Log(ctx, slog.LevelInfo, "msg", slog.String("key", getUntrustedString())) // $ hasTaintFlow="call to String" + slog.LogAttrs(ctx, slog.LevelInfo, getUntrustedString()) // $ hasValueFlow="call to getUntrustedString" + slog.LogAttrs(ctx, slog.LevelInfo, "msg", slog.String("key", getUntrustedString())) // $ hasTaintFlow="call to String" +} + +// Methods on *slog.Logger. + +func testLoggerMethods(logger *slog.Logger, ctx context.Context) { + logger.Debug(getUntrustedString()) // $ hasValueFlow="call to getUntrustedString" + logger.Info(getUntrustedString()) // $ hasValueFlow="call to getUntrustedString" + logger.Warn(getUntrustedString()) // $ hasValueFlow="call to getUntrustedString" + logger.Error(getUntrustedString()) // $ hasValueFlow="call to getUntrustedString" + logger.Info("msg", slog.Any("key", getUntrustedData())) // $ hasTaintFlow="call to Any" + logger.InfoContext(ctx, getUntrustedString()) // $ hasValueFlow="call to getUntrustedString" + logger.Log(ctx, slog.LevelInfo, getUntrustedString()) // $ hasValueFlow="call to getUntrustedString" + logger.LogAttrs(ctx, slog.LevelInfo, "msg", slog.String("key", getUntrustedString())) // $ hasTaintFlow="call to String" +} + +// With, Logger.With and Logger.WithGroup. Note that for ease of modeling we make these functions +// sinks, although strictly speaking we should consider logging functions called on the returned +// loggers as the sinks. + +func testWith(logger *slog.Logger) { + logger1 := logger.With(slog.String("key", getUntrustedString())) // $ hasTaintFlow="call to String" + logger1.Info("hello world") + logger2 := logger.With(slog.Any(getUntrustedString(), nil)) // $ hasTaintFlow="call to Any" + logger2.Info("hello world") + logger.With("key", getUntrustedData()).Info("hello world") // $ hasValueFlow="call to getUntrustedData" +} + +func testPackageWith() { + logger := slog.With(slog.String("key", getUntrustedString())) // $ hasTaintFlow="call to String" + logger.Info("hello world") + slog.With("key", getUntrustedData()).Info("hello world") // $ hasValueFlow="call to getUntrustedData" +} + +func testWithGroup(logger *slog.Logger) { + grouped := logger.WithGroup(getUntrustedString()) // $ hasValueFlow="call to getUntrustedString" + grouped.Info("hello world") +} + +// Summary models: functions relating to Attr/Value that propagate strings. + +func testAttrConstructors(logger *slog.Logger) { + logger.Info("msg", slog.Group("group", slog.String("key", getUntrustedString()))) // $ hasTaintFlow="call to Group" + logger.Info("msg", slog.GroupAttrs("group", slog.String("key", getUntrustedString()))) // $ hasTaintFlow="call to GroupAttrs" +} + +func testValueConstructors(logger *slog.Logger) { + logger.Info("msg", "key", slog.AnyValue(getUntrustedString())) // $ hasTaintFlow="call to AnyValue" + logger.Info("msg", "key", slog.StringValue(getUntrustedString())) // $ hasTaintFlow="call to StringValue" + attr := slog.String("key", getUntrustedString()) + logger.Info("msg", "key", slog.GroupValue(attr)) // $ hasTaintFlow="call to GroupValue" +} + +func testAttrAndValueAccessors(logger *slog.Logger) { + attr := slog.String("key", getUntrustedString()) + logger.Info("msg", "key", attr.String()) // $ hasTaintFlow="call to String" + + v := slog.AnyValue(getUntrustedString()) + logger.Info("msg", "key", v.Any()) // $ hasTaintFlow="call to Any" + logger.Info("msg", "key", v.String()) // $ hasTaintFlow="call to String" + + group := slog.GroupValue(slog.String("key", getUntrustedString())) + logger.Info("msg", group.Group()[0]) // $ hasTaintFlow="index expression" +}