Round-6 follow-up: UX polish + client-IP fix #26
@@ -0,0 +1,25 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// TestCSVSafe checks the CSV/spreadsheet formula-injection guard used by the admin Messages
|
||||||
|
// export: a leading formula trigger is quoted, everything else is left intact.
|
||||||
|
func TestCSVSafe(t *testing.T) {
|
||||||
|
tests := []struct{ in, want string }{
|
||||||
|
{"", ""},
|
||||||
|
{"hello", "hello"},
|
||||||
|
{"=1+1", "'=1+1"},
|
||||||
|
{"+cmd", "'+cmd"},
|
||||||
|
{"-2", "'-2"},
|
||||||
|
{"@SUM(A1)", "'@SUM(A1)"},
|
||||||
|
{"\tx", "'\tx"},
|
||||||
|
{"\rx", "'\rx"},
|
||||||
|
{"good luck", "good luck"},
|
||||||
|
{"a=b", "a=b"}, // a formula char that is not leading must be left untouched
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
if got := csvSafe(tc.in); got != tc.want {
|
||||||
|
t.Errorf("csvSafe(%q) = %q, want %q", tc.in, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -213,13 +213,29 @@ func (s *Server) consoleMessagesCSV(c *gin.Context) {
|
|||||||
w := csv.NewWriter(c.Writer)
|
w := csv.NewWriter(c.Writer)
|
||||||
_ = w.Write([]string{"time", "source", "sender_id", "sender", "ip", "message", "game_id"})
|
_ = w.Write([]string{"time", "source", "sender_id", "sender", "ip", "message", "game_id"})
|
||||||
for _, m := range items {
|
for _, m := range items {
|
||||||
|
// The sender name and message body are user-controlled; defuse spreadsheet formula
|
||||||
|
// injection so a moderator opening the export can't trigger a formula.
|
||||||
_ = w.Write([]string{
|
_ = w.Write([]string{
|
||||||
fmtTime(m.CreatedAt), m.Source, m.SenderID.String(), m.SenderName, m.SenderIP, m.Body, m.GameID.String(),
|
fmtTime(m.CreatedAt), m.Source, m.SenderID.String(), csvSafe(m.SenderName), csvSafe(m.SenderIP), csvSafe(m.Body), m.GameID.String(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
w.Flush()
|
w.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// csvSafe defuses CSV/spreadsheet formula injection: a value a spreadsheet would treat as a
|
||||||
|
// formula (a leading =, +, -, @, tab or CR) is prefixed with a single quote so it renders as
|
||||||
|
// plain text on open.
|
||||||
|
func csvSafe(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
switch s[0] {
|
||||||
|
case '=', '+', '-', '@', '\t', '\r':
|
||||||
|
return "'" + s
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
// consoleUserDetail renders one account with its stats, identities and games.
|
// consoleUserDetail renders one account with its stats, identities and games.
|
||||||
func (s *Server) consoleUserDetail(c *gin.Context) {
|
func (s *Server) consoleUserDetail(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
|||||||
Reference in New Issue
Block a user