Browse code

dockerversion: improve user-agent handling

- remove nil-check for context; passing a nil-context is never ok
- handle non-string values
- optimize escapeStr; headers should always be ASCII, so no need to
loop over runes. Also ignore newlines and control-chars.

Signed-off-by: Ritesh Vishwakarma <riteshvishwakarma.work@gmail.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Ritesh authored on 2026/04/14 02:12:42
Showing 2 changed files
... ...
@@ -2,7 +2,6 @@ package dockerversion
2 2
 
3 3
 import (
4 4
 	"context"
5
-	"fmt"
6 5
 	"runtime"
7 6
 	"strings"
8 7
 	"sync"
... ...
@@ -34,7 +33,7 @@ var (
34 34
 // getDaemonUserAgent returns the user-agent to use for requests made by
35 35
 // the daemon.
36 36
 //
37
-// It includes;
37
+// It includes:
38 38
 //
39 39
 // - the docker version
40 40
 // - go version
... ...
@@ -65,35 +64,32 @@ func getDaemonUserAgent() string {
65 65
 //
66 66
 // It returns an empty string if no user-agent is present in the context.
67 67
 func getUpstreamUserAgent(ctx context.Context) string {
68
-	var upstreamUA string
69
-	if ctx != nil {
70
-		if ki := ctx.Value(UAStringKey{}); ki != nil {
71
-			upstreamUA = ctx.Value(UAStringKey{}).(string)
72
-		}
73
-	}
74
-	if upstreamUA == "" {
68
+	upstreamUA, ok := ctx.Value(UAStringKey{}).(string)
69
+	if !ok || upstreamUA == "" {
75 70
 		return ""
76 71
 	}
77
-	return fmt.Sprintf("UpstreamClient(%s)", escapeStr(upstreamUA))
78
-}
79 72
 
80
-const charsToEscape = `();\`
73
+	return "UpstreamClient(" + escapeStr(upstreamUA) + ")"
74
+}
81 75
 
82
-// escapeStr returns s with every rune in charsToEscape escaped by a backslash
76
+// escapeStr escapes and sanitizes s for use in a User-Agent comment.
83 77
 func escapeStr(s string) string {
84
-	var ret strings.Builder
85
-	for _, currRune := range s {
86
-		appended := false
87
-		for _, escapableRune := range charsToEscape {
88
-			if currRune == escapableRune {
89
-				ret.WriteString(`\` + string(currRune))
90
-				appended = true
91
-				break
78
+	var b strings.Builder
79
+	b.Grow(len(s))
80
+
81
+	for i := range len(s) {
82
+		switch c := s[i]; c {
83
+		case '(', ')', ';', '\\':
84
+			b.WriteByte('\\')
85
+			b.WriteByte(c)
86
+		case '\t':
87
+			b.WriteByte(c)
88
+		default:
89
+			if c >= 0x20 && c != 0x7f {
90
+				b.WriteByte(c)
92 91
 			}
93 92
 		}
94
-		if !appended {
95
-			ret.WriteRune(currRune)
96
-		}
97 93
 	}
98
-	return ret.String()
94
+
95
+	return b.String()
99 96
 }
... ...
@@ -49,6 +49,11 @@ func TestDockerUserAgent(t *testing.T) {
49 49
 			ctx:      context.WithValue(t.Context(), UAStringKey{}, `Magic-Client/1.2.3 (linux); \ test`),
50 50
 			expected: getDaemonUserAgent() + ` UpstreamClient(Magic-Client/1.2.3 \(linux\)\; \\ test)`,
51 51
 		},
52
+		{
53
+			doc:      "daemon user-agent with upstream control chars",
54
+			ctx:      context.WithValue(t.Context(), UAStringKey{}, "Magic-Client/1.2.3\r\nInjected: evil"),
55
+			expected: getDaemonUserAgent() + ` UpstreamClient(Magic-Client/1.2.3Injected: evil)`,
56
+		},
52 57
 	}
53 58
 
54 59
 	for _, tc := range tests {