fix(10.1): WR-03 walk the whole partial view model before rendering
refusedViewModel compared only the top-level type with the controller's model. It now walks the type through pointers, slices, arrays, maps, struct fields and the results of exported methods, and the values held in interface-typed members, refusing the controller's model, any other GORM model (TableName, a gorm tag, gorm.Model, gorm.DeletedAt) and html/template's trusted content types anywhere in that structure.
This commit is contained in:
@@ -11,11 +11,13 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/bouncer"
|
||||
"git.golem15.com/golem15/summercms/modules/towel"
|
||||
"golang.org/x/net/html"
|
||||
"golang.org/x/net/html/atom"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// sanitizeHTML runs raw markup through the same fragment parse and allowlist
|
||||
@@ -237,16 +239,49 @@ func TestPhase101PartialSanitizer(t *testing.T) {
|
||||
Items []struct{ A template.HTMLAttr }
|
||||
}{},
|
||||
"template.URL": map[string]template.URL{},
|
||||
// WR-03: the model nested in a wrapper, behind an interface, as
|
||||
// another GORM model, or reached through a method.
|
||||
"wrapped model": struct{ Gadget *extGadget }{},
|
||||
"wrapped model slice": struct{ Rows []struct{ G extGadget } }{},
|
||||
"model in map[string]any": map[string]any{"gadget": &extGadget{Name: "top-secret"}},
|
||||
"model in []any": []any{1, extGadget{}},
|
||||
"model in an any field": struct{ Row any }{Row: []*extGadget{{}}},
|
||||
"HTML in map[string]any": map[string]any{"banner": template.HTML("<b>x</b>")},
|
||||
"another tabler model": struct{ User *BackendUser }{},
|
||||
"gorm-tagged struct": struct{ Row vmTagged }{},
|
||||
"embedded gorm.Model": struct{ vmGormModel }{},
|
||||
"gorm.DeletedAt": struct{ Deleted gorm.DeletedAt }{},
|
||||
"method returns HTML": vmHTMLMethod{},
|
||||
"pointer method HTML": struct{ Inner vmPtrHTMLMethod }{},
|
||||
"method with an argument": vmArgHTMLMethod{},
|
||||
"method returns the model": vmModelMethod{},
|
||||
} {
|
||||
if refusedViewModel(cc, vm) == "" {
|
||||
t.Fatalf("%s view model was accepted", name)
|
||||
}
|
||||
}
|
||||
selfRef := &vmNode{Label: "a"}
|
||||
selfRef.Next = selfRef
|
||||
loop := map[string]any{"n": 1}
|
||||
loop["self"] = loop
|
||||
for name, vm := range map[string]any{
|
||||
"nil": nil,
|
||||
"curated": struct{ Name string }{},
|
||||
"items": struct{ Items []extStatItem }{},
|
||||
"string": "text",
|
||||
// The shape of a curated statistics view model: labels and
|
||||
// integers, including behind interfaces.
|
||||
"stats view": struct {
|
||||
Total int
|
||||
Formats []extStatItem
|
||||
NoShelf int
|
||||
}{Total: 3, Formats: []extStatItem{{Label: "x", Count: 3}}},
|
||||
"curated map": map[string]any{"total": 3, "items": []extStatItem{{}}, "label": "x", "none": nil},
|
||||
"time field": struct{ At time.Time }{At: time.Now()},
|
||||
"self reference": selfRef,
|
||||
"cyclic map": loop,
|
||||
"plain method": vmPlainMethod{},
|
||||
"nil pointer field": struct{ Next *vmNode }{},
|
||||
} {
|
||||
if reason := refusedViewModel(cc, vm); reason != "" {
|
||||
t.Fatalf("%s view model refused: %s", name, reason)
|
||||
@@ -282,3 +317,35 @@ type leakyController struct{ extController }
|
||||
func (leakyController) PartialData(context.Context, string, any) (any, error) {
|
||||
return &extGadget{Name: "top-secret"}, nil
|
||||
}
|
||||
|
||||
// View-model fixtures of the guard (WR-03).
|
||||
type vmTagged struct {
|
||||
Secret string `gorm:"column:secret"`
|
||||
}
|
||||
|
||||
type vmGormModel struct{ gorm.Model }
|
||||
|
||||
type vmHTMLMethod struct{ Body string }
|
||||
|
||||
func (v vmHTMLMethod) Banner() template.HTML { return template.HTML(v.Body) }
|
||||
|
||||
type vmPtrHTMLMethod struct{ Body string }
|
||||
|
||||
func (v *vmPtrHTMLMethod) Banner() template.HTML { return template.HTML(v.Body) }
|
||||
|
||||
type vmArgHTMLMethod struct{}
|
||||
|
||||
func (vmArgHTMLMethod) Wrap(s string) template.HTML { return template.HTML(s) }
|
||||
|
||||
type vmModelMethod struct{}
|
||||
|
||||
func (vmModelMethod) Gadget() *extGadget { return &extGadget{} }
|
||||
|
||||
type vmPlainMethod struct{ N int }
|
||||
|
||||
func (v vmPlainMethod) Double() int { return v.N * 2 }
|
||||
|
||||
type vmNode struct {
|
||||
Label string
|
||||
Next *vmNode
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user