diff --git a/go/ql/lib/semmle/go/security/LogInjectionCustomizations.qll b/go/ql/lib/semmle/go/security/LogInjectionCustomizations.qll index 9160c204df1d..3d923855cfa6 100644 --- a/go/ql/lib/semmle/go/security/LogInjectionCustomizations.qll +++ b/go/ql/lib/semmle/go/security/LogInjectionCustomizations.qll @@ -84,4 +84,33 @@ module LogInjection { ) } } -} + + /** + * Returns true if `t` is a zap encoder type that is considered safe. + * + * We intentionally whitelist *only* JSONEncoder. + * Other encoders may not escape newline characters and therefore + * must NOT be treated as sanitizers. + */ + private predicate isSafeZapEncoder(Type t) { + exists(Type zapEncoder | + // Matches go.uber.org/zap/zapcore.JSONEncoder + zapEncoder.hasQualifiedName("go.uber.org/zap/zapcore", "JSONEncoder") and + t = zapEncoder + ) + } + + /** + * Zap encoder sanitizer class. + * + * This extends the Sanitizer class used by the go/log-injection query. + */ + class ZapEncoderSanitizer extends Sanitizer { + ZapEncoderSanitizer() { + exists(Type t | + this.getType() = t and + isSafeZapEncoder(t) + ) + } + } +} \ No newline at end of file diff --git a/go/ql/test/query-tests/Security/CWE-117/LogInjection.go b/go/ql/test/query-tests/Security/CWE-117/LogInjection.go index fc9d71791582..c9e9b7027fec 100644 --- a/go/ql/test/query-tests/Security/CWE-117/LogInjection.go +++ b/go/ql/test/query-tests/Security/CWE-117/LogInjection.go @@ -724,3 +724,23 @@ func handlerGood5(req *http.Request) { object := req.URL.Query()["username"][0] log.Printf("found object of type %T.\n", object) } + +// UNSAFE: zap.NewProduction() uses production encoder but is NOT considered a sanitizer by our customization. +func zapUnsafeExample(req *http.Request) { + username := req.URL.Query()["username"][0] + + logger, _ := zap.NewProduction() + logger.Info("Login attempt by:", username) // $ hasTaintFlow="username" +} + +// GOOD: zap logger using a JSONEncoder that our customization treats as a sanitizer. +func zapSafeExample(req *http.Request) { + username := req.URL.Query()["username"][0] + + cfg := zap.NewProductionConfig() + // Explicitly force JSONEncoder so that your sanitizer will match it. + cfg.Encoding = "json" + + logger, _ := cfg.Build() + logger.Info("Login attempt", zap.String("username", username)) // NO finding expected +} \ No newline at end of file