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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user