detached 51fda06

fix(broker): allow control characters in environment values (#285)

commit
51fda0688e038db64b74e462585e39ced6f7f4e6
author
Brice Amen · · 2026-09-25 19:44 UTC
committer
GitHub · · 2026-09-25 19:44 UTC
parent
8ea43d2
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
3 files · +11 -3
M internal/adapters/daemonmux/codec_test.go +2 -2
@@ -527,7 +527,7 @@ func TestMuxValidationNegatives(t *testing.T) {
527527 _, err := EncodeServer(Refused{Ref: testRef(5), Error: boundary}, testEnvelopeCeiling, testChunkCeiling)
528528 require.NoError(t, err)
529529 })
530- t.Run("bidi and control text refused", func(t *testing.T) {
530+ t.Run("display control text and env NUL refused", func(t *testing.T) {
531531 evil := testErrorDetail()
532532 evil.Text = "timeout\u202erorrE"
533533 _, err := EncodeServer(Refused{Ref: testRef(5), Error: evil}, testEnvelopeCeiling, testChunkCeiling)
@@ -537,7 +537,7 @@ func TestMuxValidationNegatives(t *testing.T) {
537537 require.ErrorIs(t, err, ErrInvalidMessage)
538538
539539 badEnv := testOpen()
540- badEnv.Env = []string{"K=\u202eevil"}
540+ badEnv.Env = []string{"K=bad\x00value"}
541541 _, err = EncodeClient(badEnv, testEnvelopeCeiling, testChunkCeiling)
542542 require.ErrorIs(t, err, ErrInvalidMessage)
543543 })
@@ -527,7 +527,7 @@ func TestMuxValidationNegatives(t *testing.T) {
527 _, err := EncodeServer(Refused{Ref: testRef(5), Error: boundary}, testEnvelopeCeiling, testChunkCeiling)527 _, err := EncodeServer(Refused{Ref: testRef(5), Error: boundary}, testEnvelopeCeiling, testChunkCeiling)
528 require.NoError(t, err)528 require.NoError(t, err)
529 })529 })
530- t.Run("bidi and control text refused", func(t *testing.T) {530+ t.Run("display control text and env NUL refused", func(t *testing.T) {
531 evil := testErrorDetail()531 evil := testErrorDetail()
532 evil.Text = "timeout\u202erorrE"532 evil.Text = "timeout\u202erorrE"
533 _, err := EncodeServer(Refused{Ref: testRef(5), Error: evil}, testEnvelopeCeiling, testChunkCeiling)533 _, err := EncodeServer(Refused{Ref: testRef(5), Error: evil}, testEnvelopeCeiling, testChunkCeiling)
@@ -537,7 +537,7 @@ func TestMuxValidationNegatives(t *testing.T) {
537 require.ErrorIs(t, err, ErrInvalidMessage)537 require.ErrorIs(t, err, ErrInvalidMessage)
538 538
539 badEnv := testOpen()539 badEnv := testOpen()
540- badEnv.Env = []string{"K=\u202eevil"}540+ badEnv.Env = []string{"K=bad\x00value"}
541 _, err = EncodeClient(badEnv, testEnvelopeCeiling, testChunkCeiling)541 _, err = EncodeClient(badEnv, testEnvelopeCeiling, testChunkCeiling)
542 require.ErrorIs(t, err, ErrInvalidMessage)542 require.ErrorIs(t, err, ErrInvalidMessage)
543 })543 })
M internal/adapters/protoconv/broker_convert.go +7 -1
@@ -130,7 +130,13 @@ func BrokerDisplayText(value string, maxBytes int) error {
130130 }
131131
132132 func BrokerEnvEntry(entry string) error {
133- return BrokerDisplayText(entry, ports.BrokerMaxEnvEntryBytes)
133+ if len(entry) > ports.BrokerMaxEnvEntryBytes {
134+ return ErrTooLarge
135+ }
136+ if !utf8.ValidString(entry) || strings.ContainsRune(entry, 0) {
137+ return ErrInvalid
138+ }
139+ return nil
134140 }
135141
136142 func BrokerEnvEntries(env []string) error {
@@ -130,7 +130,13 @@ func BrokerDisplayText(value string, maxBytes int) error {
130 }130 }
131 131
132 func BrokerEnvEntry(entry string) error {132 func BrokerEnvEntry(entry string) error {
133- return BrokerDisplayText(entry, ports.BrokerMaxEnvEntryBytes)133+ if len(entry) > ports.BrokerMaxEnvEntryBytes {
134+ return ErrTooLarge
135+ }
136+ if !utf8.ValidString(entry) || strings.ContainsRune(entry, 0) {
137+ return ErrInvalid
138+ }
139+ return nil
134 }140 }
135 141
136 func BrokerEnvEntries(env []string) error {142 func BrokerEnvEntries(env []string) error {
M internal/adapters/protoconv/broker_convert_test.go +2 -0
@@ -98,6 +98,8 @@ func TestBrokerConversionFailures(t *testing.T) {
9898 {"invalid utf8", func() error { return BrokerDisplayText("\xff", 20) }, ErrInvalid},
9999 {"oversize env", func() error { return BrokerEnvEntries([]string{strings.Repeat("a", ports.BrokerMaxEnvEntryBytes+1)}) }, ErrTooLarge},
100100 {"missing equals", func() error { return BrokerEnvEntries([]string{"TERM"}) }, ErrInvalid},
101+ {"nul env", func() error { return BrokerEnvEntries([]string{"A=x\x00y"}) }, ErrInvalid},
102+ {"ansi prompt env", func() error { return BrokerEnvEntries([]string{"PS1=\x1b[32m$ \x1b[0m"}) }, nil},
101103 {"valid env", func() error { return BrokerEnvEntries([]string{"TERM=x"}) }, nil},
102104 } {
103105 t.Run(tc.name, func(t *testing.T) {
@@ -98,6 +98,8 @@ func TestBrokerConversionFailures(t *testing.T) {
98 {"invalid utf8", func() error { return BrokerDisplayText("\xff", 20) }, ErrInvalid},98 {"invalid utf8", func() error { return BrokerDisplayText("\xff", 20) }, ErrInvalid},
99 {"oversize env", func() error { return BrokerEnvEntries([]string{strings.Repeat("a", ports.BrokerMaxEnvEntryBytes+1)}) }, ErrTooLarge},99 {"oversize env", func() error { return BrokerEnvEntries([]string{strings.Repeat("a", ports.BrokerMaxEnvEntryBytes+1)}) }, ErrTooLarge},
100 {"missing equals", func() error { return BrokerEnvEntries([]string{"TERM"}) }, ErrInvalid},100 {"missing equals", func() error { return BrokerEnvEntries([]string{"TERM"}) }, ErrInvalid},
101+ {"nul env", func() error { return BrokerEnvEntries([]string{"A=x\x00y"}) }, ErrInvalid},
102+ {"ansi prompt env", func() error { return BrokerEnvEntries([]string{"PS1=\x1b[32m$ \x1b[0m"}) }, nil},
101 {"valid env", func() error { return BrokerEnvEntries([]string{"TERM=x"}) }, nil},103 {"valid env", func() error { return BrokerEnvEntries([]string{"TERM=x"}) }, nil},
102 } {104 } {
103 t.Run(tc.name, func(t *testing.T) {105 t.Run(tc.name, func(t *testing.T) {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close