diff --git a/app/rest/api/admin.go b/app/rest/api/admin.go index 1ae38e1d..386b6d98 100644 --- a/app/rest/api/admin.go +++ b/app/rest/api/admin.go @@ -126,7 +126,7 @@ func (a *admin) checkBlocked(siteID string, user store.User) bool { return a.dataService.IsBlocked(siteID, user.ID) } -// processes comments and hides text of all comments for blocked users. +// post-processes comments, hides text of all comments for blocked users, // resets score and votes too. Also hides sensitive info for non-admin users func (a *admin) alterComments(comments []store.Comment, r *http.Request) (res []store.Comment) { res = make([]store.Comment, len(comments)) @@ -145,11 +145,6 @@ func (a *admin) alterComments(comments []store.Comment, r *http.Request) (res [] c.Deleted = true } - // set default avatar - if c.User.Picture == "" { - c.User.Picture = a.defAvatarURL - } - // hide info from non-admins if !isAdmin { c.User.IP = "" diff --git a/app/rest/api/rest.go b/app/rest/api/rest.go index bdc7a93d..2036797c 100644 --- a/app/rest/api/rest.go +++ b/app/rest/api/rest.go @@ -36,8 +36,8 @@ type Rest struct { Cache rest.LoadingCache WebRoot string - httpServer *http.Server - mod admin + httpServer *http.Server + amdminService admin } // Run the lister and request's router, activate rest server @@ -48,6 +48,10 @@ func (s *Rest) Run(port int) { log.Printf("[DEBUG] admins %+v", s.Authenticator.Admins) } + s.amdminService = admin{dataService: s.DataService, exporter: s.Exporter, cache: s.Cache, + defAvatarURL: s.Authenticator.AvatarProxy.Default(), + } + router := chi.NewRouter() router.Use(middleware.RealIP, Recoverer) router.Use(middleware.Throttle(1000), middleware.Timeout(60*time.Second)) @@ -92,14 +96,9 @@ func (s *Rest) Run(port int) { rauth.Put("/comment/{id}", s.updateCommentCtrl) rauth.Get("/user", s.userInfoCtrl) rauth.Put("/vote/{id}", s.voteCtrl) + // admin routes, admin users only - s.mod = admin{ - dataService: s.DataService, - exporter: s.Exporter, - cache: s.Cache, - defAvatarURL: s.Authenticator.AvatarProxy.Default(), - } - rauth.Mount("/admin", s.mod.routes(s.Authenticator.AdminOnly)) + rauth.Mount("/admin", s.amdminService.routes(s.Authenticator.AdminOnly)) }) }) @@ -129,14 +128,14 @@ func (s *Rest) createCommentCtrl(w http.ResponseWriter, r *http.Request) { return } - comment.Prepare() // clean all fields user not suppoed to set + comment.PrepareUntrusted() // clean all fields user not suppoed to set comment.User = user comment.User.IP = strings.Split(r.RemoteAddr, ":")[0] comment.Text = string(blackfriday.Run([]byte(comment.Text), blackfriday.WithNoExtensions())) // render markdown log.Printf("[DEBUG] create comment %+v", comment) // check if user blocked - if s.mod.checkBlocked(comment.Locator.SiteID, comment.User) { + if s.amdminService.checkBlocked(comment.Locator.SiteID, comment.User) { rest.SendErrorJSON(w, r, http.StatusForbidden, errors.New("rejected"), "user blocked") return } @@ -220,7 +219,7 @@ func (s *Rest) findCommentsCtrl(w http.ResponseWriter, r *http.Request) { if e != nil { return nil, e } - maskedComments := s.mod.alterComments(comments, r) + maskedComments := s.amdminService.alterComments(comments, r) var b []byte switch r.URL.Query().Get("format") { case "tree": @@ -253,7 +252,7 @@ func (s *Rest) lastCommentsCtrl(w http.ResponseWriter, r *http.Request) { if e != nil { return nil, e } - comments = s.mod.alterComments(comments, r) + comments = s.amdminService.alterComments(comments, r) return encodeJSONWithHTML(comments) }) @@ -278,7 +277,7 @@ func (s *Rest) commentByIDCtrl(w http.ResponseWriter, r *http.Request) { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get comment by id") return } - comment = s.mod.alterComments([]store.Comment{comment}, r)[0] + comment = s.amdminService.alterComments([]store.Comment{comment}, r)[0] render.Status(r, http.StatusOK) renderJSONWithHTML(w, r, comment) } @@ -306,7 +305,7 @@ func (s *Rest) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) { if e != nil { return nil, e } - comments = s.mod.alterComments(comments, r) + comments = s.amdminService.alterComments(comments, r) resp.Comments, resp.Count = comments, count return encodeJSONWithHTML(resp) }) diff --git a/app/store/comment.go b/app/store/comment.go index 965e8a9c..5cd4b5bb 100644 --- a/app/store/comment.go +++ b/app/store/comment.go @@ -1,7 +1,11 @@ package store import ( + "crypto/sha1" + "fmt" + "hash/crc64" "html/template" + "strconv" "strings" "time" @@ -59,9 +63,9 @@ type BlockedUser struct { Timestamp time.Time `json:"time"` } -// Prepare comment received from untrusted source by clearing all autogen fields and -// reset everyting users not supposed to provide -func (c *Comment) Prepare() { +// PrepareUntrusted preprocess comment received from untrusted source by clearing all +// autogen fields and reset everyting users not supposed to provide +func (c *Comment) PrepareUntrusted() { c.ID = "" // don't allow user to define ID, force auto-gen c.Timestamp = time.Time{} // reset time, force auto-gen c.Votes = make(map[string]bool) @@ -84,6 +88,25 @@ func (c *Comment) Sanitize() { c.Text = strings.Replace(c.Text, "\t", "", -1) } +// HashUserFields replace sensitive fields with hashes +func (c *Comment) HashUserFields() { + + hashVal := func(val string) string { + if _, err := strconv.ParseUint(val, 16, 64); err == nil || val == "" { + return val // already hashed + } + h := sha1.New() + if _, err := h.Write([]byte(val)); err != nil { + // fail back to crc64 + return fmt.Sprintf("%x", crc64.Checksum([]byte(val), crc64.MakeTable(crc64.ECMA))) + } + return fmt.Sprintf("%x", h.Sum(nil)) + } + + c.User.IP = hashVal(c.User.IP) + c.User.ID = hashVal(c.User.ID) +} + // SetDeleted clears comment info, reset to deleted state func (c *Comment) SetDeleted() { c.Text = "" diff --git a/app/store/comment_test.go b/app/store/comment_test.go index e78dcad8..dbfe61a9 100644 --- a/app/store/comment_test.go +++ b/app/store/comment_test.go @@ -32,7 +32,7 @@ func TestComment_Sanitize(t *testing.T) { } } -func TestComment_Prepare(t *testing.T) { +func TestComment_PrepareUntrusted(t *testing.T) { comment := Comment{ Text: `blah`, User: User{ID: "username"}, @@ -46,7 +46,7 @@ func TestComment_Prepare(t *testing.T) { Votes: map[string]bool{"uu": true}, } - comment.Prepare() + comment.PrepareUntrusted() assert.Equal(t, "", comment.ID) assert.Equal(t, "p123", comment.ParentID) assert.Equal(t, "blah", comment.Text) @@ -58,3 +58,27 @@ func TestComment_Prepare(t *testing.T) { assert.Equal(t, User{ID: "username"}, comment.User) } + +func TestComment_HashUserFields(t *testing.T) { + tbl := []struct { + inp Comment + out Comment + }{ + {inp: Comment{}, out: Comment{}}, + { + inp: Comment{ + Text: "blah", + User: User{ID: "my id", IP: "127.0.0.1"}, + }, + out: Comment{ + Text: "blah", + User: User{ID: "de58071dda71e1783b6deb931ddb48bb66966f79", IP: "4b84b15bff6ee5796152495a230e45e3d7e947d9"}, + }, + }, + } + + for n, tt := range tbl { + tt.inp.HashUserFields() + assert.Equal(t, tt.out, tt.inp, "check #%d", n) + } +}