Skip to content

Commit 812d72c

Browse files
authored
fix: sanitize app status summary (#19075)
Fixes #18875
1 parent 29486f9 commit 812d72c

File tree

5 files changed

+86
-3
lines changed

5 files changed

+86
-3
lines changed

coderd/util/strings/strings.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ package strings
22

33
import (
44
"fmt"
5+
"strconv"
56
"strings"
7+
"unicode"
8+
9+
"github.com/acarl005/stripansi"
10+
"github.com/microcosm-cc/bluemonday"
611
)
712

813
// JoinWithConjunction joins a slice of strings with commas except for the last
@@ -28,3 +33,38 @@ func Truncate(s string, n int) string {
2833
}
2934
return s[:n]
3035
}
36+
37+
var bmPolicy = bluemonday.StrictPolicy()
38+
39+
// UISanitize sanitizes a string for display in the UI.
40+
// The following transformations are applied, in order:
41+
// - HTML tags are removed using bluemonday's strict policy.
42+
// - ANSI escape codes are stripped using stripansi.
43+
// - Consecutive backslashes are replaced with a single backslash.
44+
// - Non-printable characters are removed.
45+
// - Whitespace characters are replaced with spaces.
46+
// - Multiple spaces are collapsed into a single space.
47+
// - Leading and trailing whitespace is trimmed.
48+
func UISanitize(in string) string {
49+
if unq, err := strconv.Unquote(`"` + in + `"`); err == nil {
50+
in = unq
51+
}
52+
in = bmPolicy.Sanitize(in)
53+
in = stripansi.Strip(in)
54+
var b strings.Builder
55+
var spaceSeen bool
56+
for _, r := range in {
57+
if unicode.IsSpace(r) {
58+
if !spaceSeen {
59+
_, _ = b.WriteRune(' ')
60+
spaceSeen = true
61+
}
62+
continue
63+
}
64+
spaceSeen = false
65+
if unicode.IsPrint(r) {
66+
_, _ = b.WriteRune(r)
67+
}
68+
}
69+
return strings.TrimSpace(b.String())
70+
}

coderd/util/strings/strings_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package strings_test
33
import (
44
"testing"
55

6+
"github.com/stretchr/testify/assert"
67
"github.com/stretchr/testify/require"
78

89
"github.com/coder/coder/v2/coderd/util/strings"
@@ -37,3 +38,41 @@ func TestTruncate(t *testing.T) {
3738
})
3839
}
3940
}
41+
42+
func TestUISanitize(t *testing.T) {
43+
t.Parallel()
44+
45+
for _, tt := range []struct {
46+
s string
47+
expected string
48+
}{
49+
{"normal text", "normal text"},
50+
{"\tfoo \r\\nbar ", "foo bar"},
51+
{"通常のテキスト", "通常のテキスト"},
52+
{"foo\nbar", "foo bar"},
53+
{"foo\tbar", "foo bar"},
54+
{"foo\rbar", "foo bar"},
55+
{"foo\x00bar", "foobar"},
56+
{"\u202Eabc", "abc"},
57+
{"\u200Bzero width", "zero width"},
58+
{"foo\x1b[31mred\x1b[0mbar", "fooredbar"},
59+
{"foo\u0008bar", "foobar"},
60+
{"foo\x07bar", "foobar"},
61+
{"foo\uFEFFbar", "foobar"},
62+
{"<a href='javascript:alert(1)'>link</a>", "link"},
63+
{"<style>body{display:none}</style>", ""},
64+
{"<html>HTML</html>", "HTML"},
65+
{"<br>line break", "line break"},
66+
{"<link rel='stylesheet' href='evil.css'>", ""},
67+
{"<img src=1 onerror=alert(1)>", ""},
68+
{"<!-- comment -->visible", "visible"},
69+
{"<script>alert('xss')</script>", ""},
70+
{"<iframe src='evil.com'></iframe>", ""},
71+
} {
72+
t.Run(tt.expected, func(t *testing.T) {
73+
t.Parallel()
74+
actual := strings.UISanitize(tt.s)
75+
assert.Equal(t, tt.expected, actual)
76+
})
77+
}
78+
}

coderd/workspaceagents.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
"github.com/coder/coder/v2/coderd/rbac/policy"
4242
"github.com/coder/coder/v2/coderd/telemetry"
4343
maputil "github.com/coder/coder/v2/coderd/util/maps"
44+
strutil "github.com/coder/coder/v2/coderd/util/strings"
4445
"github.com/coder/coder/v2/coderd/wspubsub"
4546
"github.com/coder/coder/v2/codersdk"
4647
"github.com/coder/coder/v2/codersdk/agentsdk"
@@ -383,6 +384,9 @@ func (api *API) patchWorkspaceAgentAppStatus(rw http.ResponseWriter, r *http.Req
383384
return
384385
}
385386

387+
// Treat the message as untrusted input.
388+
cleaned := strutil.UISanitize(req.Message)
389+
386390
// nolint:gocritic // This is a system restricted operation.
387391
_, err = api.Database.InsertWorkspaceAppStatus(dbauthz.AsSystemRestricted(ctx), database.InsertWorkspaceAppStatusParams{
388392
ID: uuid.New(),
@@ -391,7 +395,7 @@ func (api *API) patchWorkspaceAgentAppStatus(rw http.ResponseWriter, r *http.Req
391395
AgentID: workspaceAgent.ID,
392396
AppID: app.ID,
393397
State: database.WorkspaceAppStatusState(req.State),
394-
Message: req.Message,
398+
Message: cleaned,
395399
Uri: sql.NullString{
396400
String: req.URI,
397401
Valid: req.URI != "",

codersdk/toolsdk/toolsdk.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ ONLY report an "idle" or "failure" state if you have FULLY completed the task.
229229
Properties: map[string]any{
230230
"summary": map[string]any{
231231
"type": "string",
232-
"description": "A concise summary of your current progress on the task. This must be less than 160 characters in length.",
232+
"description": "A concise summary of your current progress on the task. This must be less than 160 characters in length and must not include newlines or other control characters.",
233233
},
234234
"link": map[string]any{
235235
"type": "string",

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ require (
365365
github.com/mdlayher/netlink v1.7.2 // indirect
366366
github.com/mdlayher/sdnotify v1.0.0 // indirect
367367
github.com/mdlayher/socket v0.5.0 // indirect
368-
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
368+
github.com/microcosm-cc/bluemonday v1.0.27
369369
github.com/miekg/dns v1.1.57 // indirect
370370
github.com/mitchellh/copystructure v1.2.0 // indirect
371371
github.com/mitchellh/go-homedir v1.1.0 // indirect

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy