Sanitize actor displaynames (#4864)

* Sanitize AP name possible XSS

* Pull out sanitizing method + remove rendering markdown for AP chat messages

* Add tests around AP message rendering and usernames

* Add sanitization tests to AP integration test
This commit is contained in:
Gabe Kangas
2026-03-31 20:33:05 -07:00
committed by GitHub
parent a2981903c2
commit e738156fd7
7 changed files with 783 additions and 109 deletions
+13 -4
View File
@@ -4,12 +4,24 @@ import (
"fmt"
"github.com/go-fed/activity/streams/vocab"
"github.com/microcosm-cc/bluemonday"
"github.com/owncast/owncast/activitypub/resolvers"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/persistence/configrepository"
)
// sanitizeActorName strips HTML tags from the ActivityPub actor display name.
// Falls back to the username if the display name is empty or entirely HTML.
func sanitizeActorName(displayName, username string) string {
strict := bluemonday.StrictPolicy()
name := strict.Sanitize(displayName)
if name == "" {
name = strict.Sanitize(username)
}
return name
}
func handleEngagementActivity(eventType events.EventType, isLiveNotification bool, actorReference vocab.ActivityStreamsActorProperty, action string) error {
configRepository := configrepository.Get()
@@ -30,10 +42,7 @@ func handleEngagementActivity(eventType events.EventType, isLiveNotification boo
}
// Send chat message
actorName := actor.Name
if actorName == "" {
actorName = actor.Username
}
actorName := sanitizeActorName(actor.Name, actor.Username)
actorIRI := actor.ActorIriString()
userPrefix := fmt.Sprintf("%s ", actorName)
+120
View File
@@ -0,0 +1,120 @@
package inbox
import (
"testing"
)
func TestSanitizeActorName(t *testing.T) {
tests := []struct {
name string
displayName string
username string
expected string
}{
{
name: "plain display name",
displayName: "Alice",
username: "alice",
expected: "Alice",
},
{
name: "display name with emoji",
displayName: "Alice 🦊",
username: "alice",
expected: "Alice 🦊",
},
{
name: "display name with unicode",
displayName: "Ålice Böb",
username: "alice",
expected: "Ålice Böb",
},
{
name: "empty display name falls back to username",
displayName: "",
username: "alice",
expected: "alice",
},
{
name: "script tag in display name",
displayName: `<script>alert("xss")</script>`,
username: "alice",
expected: "alice",
},
{
name: "iframe injection in display name",
displayName: `<iframe src="https://evil.com" style="position:fixed;top:0;left:0;width:100%;height:100%"></iframe>`,
username: "alice",
expected: "alice",
},
{
name: "img tag in display name",
displayName: `<img src="https://evil.com/track.png">`,
username: "alice",
expected: "alice",
},
{
name: "form injection in display name",
displayName: `<form action="https://evil.com/steal"><input name="pw" type="password"></form>`,
username: "alice",
expected: "alice",
},
{
name: "meta refresh in display name",
displayName: `<meta http-equiv="refresh" content="0;url=https://evil.com">`,
username: "alice",
expected: "alice",
},
{
name: "mixed text and HTML in display name",
displayName: `Alice <script>alert(1)</script> Bob`,
username: "alice",
expected: "Alice Bob",
},
{
name: "custom emoji HTML in display name",
displayName: `Alice :blobcat: <img src="https://instance.com/emoji/blobcat.png" class="custom-emoji">`,
username: "alice",
expected: "Alice :blobcat: ",
},
{
name: "HTML in both display name and username",
displayName: `<script>alert(1)</script>`,
username: `<b>alice</b>`,
expected: "alice",
},
{
name: "entirely HTML display name falls back to username",
displayName: `<div></div>`,
username: "alice",
expected: "alice",
},
{
name: "style tag in display name",
displayName: `<style>body{display:none}</style>Alice`,
username: "alice",
expected: "Alice",
},
{
name: "nested HTML tags",
displayName: `<div><span><a href="https://evil.com">Click me</a></span></div>`,
username: "alice",
expected: "Click me",
},
{
name: "event handler attributes",
displayName: `<img src=x onerror="alert(1)">Alice`,
username: "alice",
expected: "Alice",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := sanitizeActorName(tt.displayName, tt.username)
if result != tt.expected {
t.Errorf("sanitizeActorName(%q, %q) = %q, want %q", tt.displayName, tt.username, result, tt.expected)
}
})
}
}