Sanitize text record values
This commit is contained in:
parent
0c72e17d94
commit
ee850124f6
@ -50,6 +50,7 @@ type (
|
||||
|
||||
recordValuesSanitizer interface {
|
||||
Run(*types.Module, types.RecordValueSet) types.RecordValueSet
|
||||
RunXSS(*types.Module, types.RecordValueSet) types.RecordValueSet
|
||||
}
|
||||
|
||||
recordValuesValidator interface {
|
||||
@ -233,6 +234,7 @@ func (svc record) lookup(ctx context.Context, namespaceID, moduleID uint64, look
|
||||
}
|
||||
|
||||
r.SetModule(m)
|
||||
r.Values = svc.sanitizer.RunXSS(m, r.Values)
|
||||
|
||||
return nil
|
||||
}()
|
||||
@ -312,6 +314,7 @@ func (svc record) Find(ctx context.Context, filter types.RecordFilter) (set type
|
||||
|
||||
_ = set.Walk(func(r *types.Record) error {
|
||||
r.SetModule(m)
|
||||
r.Values = svc.sanitizer.RunXSS(m, r.Values)
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
@ -2,12 +2,14 @@ package values
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/expr"
|
||||
"github.com/cortezaproject/corteza-server/pkg/logger"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/compose/types"
|
||||
@ -117,7 +119,7 @@ func (s sanitizer) Run(m *types.Module, vv types.RecordValueSet) (out types.Reco
|
||||
}
|
||||
|
||||
// Per field type validators
|
||||
switch strings.ToLower(f.Kind) {
|
||||
switch kind {
|
||||
case "bool":
|
||||
v.Value = sBool(v.Value)
|
||||
|
||||
@ -127,6 +129,9 @@ func (s sanitizer) Run(m *types.Module, vv types.RecordValueSet) (out types.Reco
|
||||
case "number":
|
||||
v.Value = sNumber(v.Value, f.Options.Precision())
|
||||
|
||||
case "string":
|
||||
v.Value = sString(v.Value)
|
||||
|
||||
// Uncomment when they become relevant for sanitization
|
||||
//case "email":
|
||||
// v = s.sEmail(v, f, m)
|
||||
@ -136,8 +141,6 @@ func (s sanitizer) Run(m *types.Module, vv types.RecordValueSet) (out types.Reco
|
||||
// v = s.sRecord(v, f, m)
|
||||
//case "select":
|
||||
// v = s.sSelect(v, f, m)
|
||||
//case "string":
|
||||
// v = s.sString(v, f, m)
|
||||
//case "url":
|
||||
// v = s.sUrl(v, f, m)
|
||||
//case "user":
|
||||
@ -148,6 +151,29 @@ func (s sanitizer) Run(m *types.Module, vv types.RecordValueSet) (out types.Reco
|
||||
return
|
||||
}
|
||||
|
||||
func (s sanitizer) RunXSS(m *types.Module, vv types.RecordValueSet) types.RecordValueSet {
|
||||
var (
|
||||
f *types.ModuleField
|
||||
)
|
||||
|
||||
for _, v := range vv {
|
||||
f = m.Fields.FindByName(v.Name)
|
||||
if f == nil {
|
||||
// Unknown field,
|
||||
// if it is not handled before,
|
||||
// sanitizer does not care about it
|
||||
continue
|
||||
}
|
||||
|
||||
switch strings.ToLower(f.Kind) {
|
||||
case "string":
|
||||
v.Value = sString(v.Value)
|
||||
}
|
||||
}
|
||||
|
||||
return vv
|
||||
}
|
||||
|
||||
func sBool(v interface{}) string {
|
||||
switch c := v.(type) {
|
||||
case bool:
|
||||
@ -258,6 +284,19 @@ func sNumber(num interface{}, p uint) string {
|
||||
return str
|
||||
}
|
||||
|
||||
// sString is used mostly to strip insecure html data
|
||||
// from strings
|
||||
func sString(str string) string {
|
||||
// use standard html escaping policy
|
||||
p := bluemonday.UGCPolicy()
|
||||
|
||||
// match only colors for html editor elements on style attr
|
||||
p.AllowAttrs("style").OnElements("span", "p")
|
||||
p.AllowStyles("color").Matching(regexp.MustCompile("(?i)^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$")).Globally()
|
||||
|
||||
return p.Sanitize(str)
|
||||
}
|
||||
|
||||
// sanitize casts value to field kind format
|
||||
func sanitize(f *types.ModuleField, v interface{}) string {
|
||||
switch strings.ToLower(f.Kind) {
|
||||
|
||||
@ -2,11 +2,12 @@ package values
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/compose/types"
|
||||
)
|
||||
|
||||
@ -160,6 +161,160 @@ func Test_sanitizer_Run(t *testing.T) {
|
||||
input: "42.040",
|
||||
output: "42.04",
|
||||
},
|
||||
{
|
||||
name: "string escaping; html",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: "<span onerror=alert()>Title here</span>",
|
||||
output: "<span>Title here</span>",
|
||||
},
|
||||
{
|
||||
name: "string escaping; html a.href with javascript alert",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `<a href="javascript:alert('XSS1')" onmouseover="alert('XSS2')">XSS<a>`,
|
||||
output: "XSS",
|
||||
},
|
||||
{
|
||||
name: "string escaping; a.href with javascript",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `<a href="javascript:document.location='https://cortezaproject.org/'">XSS</A>`,
|
||||
output: "XSS",
|
||||
},
|
||||
{
|
||||
name: "string escaping; script with script",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `<script>document.write("<scri");</script>pt src="https://cortezaproject.org/script.js"></script>`,
|
||||
output: "pt src="https://cortezaproject.org/script.js">",
|
||||
},
|
||||
{
|
||||
name: "string escaping; script with a",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `<script a=">'>" src="https://cortezaproject.org/xss.js"></script>`,
|
||||
output: "",
|
||||
},
|
||||
{
|
||||
name: "string escaping; meta with script",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `<meta http-equiv="set-cookie" content="<script>alert('xss')</script>">`,
|
||||
output: "",
|
||||
},
|
||||
{
|
||||
name: "string escaping; object",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `<object type="text/x-scriptlet" data="https://cortezaproject.org/xss.html"></object>`,
|
||||
output: "",
|
||||
},
|
||||
{
|
||||
name: "string escaping; base href",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `<base href="javascript:alert('xss');//">`,
|
||||
output: "",
|
||||
},
|
||||
{
|
||||
name: "string escaping; script",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `<!--[if gte ie 4]><script>alert('xss');</script><![endif]-->`,
|
||||
output: "",
|
||||
},
|
||||
{
|
||||
name: "string escaping; div",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `<div style="background-image: url(javascript:alert('XSS'))"></div>`,
|
||||
output: "<div></div>",
|
||||
},
|
||||
{
|
||||
name: "string escaping; frameset",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `<frameset><frame src="javascript:alert('XSS');"></frameset>`,
|
||||
output: "",
|
||||
},
|
||||
{
|
||||
name: "string escaping; iframe",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `<iframe src=# onmouseover="alert(document.cookie)"></iframe>`,
|
||||
output: "",
|
||||
},
|
||||
{
|
||||
name: "string escaping; meta",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `<meta http-equiv="refresh" content="0; url=https://;url=javascript:alert('xss');">`,
|
||||
output: "",
|
||||
},
|
||||
{
|
||||
name: "string escaping; br",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `<br size="&{alert('XSS')}">`,
|
||||
output: "<br>",
|
||||
},
|
||||
{
|
||||
name: "string escaping; bgsound",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `<bgsound src="javascript:alert('XSS');">`,
|
||||
output: "",
|
||||
},
|
||||
{
|
||||
name: "string escaping; input type image",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `<input type="image" src="javascript:alert('XSS');">`,
|
||||
output: "",
|
||||
},
|
||||
{
|
||||
name: "string escaping; style",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `<style>@import 'https://cortezaproject.org/xss.css';</style>`,
|
||||
output: "",
|
||||
},
|
||||
{
|
||||
name: "string escaping; link",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `<link rel="stylesheet" href="javascript:alert('xss');">`,
|
||||
output: "",
|
||||
},
|
||||
{
|
||||
name: "string escaping; html body onload event",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `<body onload=alert('XSS')>`,
|
||||
output: "",
|
||||
},
|
||||
{
|
||||
name: "string escaping; xss element",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `'';!--"<XSS>=&{()}`,
|
||||
output: "'';!--"=&{()}",
|
||||
},
|
||||
{
|
||||
name: "string escaping; xss element",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `Hello <span class="><script src='https://cortezaproject.org/XSS.js'></script>">there</span> world.`,
|
||||
output: "Hello <span>there</span> world.",
|
||||
},
|
||||
{
|
||||
name: "string escaping; xss element",
|
||||
kind: "String",
|
||||
options: map[string]interface{}{},
|
||||
input: `<tag1>cor<tag2></tag2>teza</tag1><tag1>server</tag1><tag2>123</tag2>`,
|
||||
output: "cortezaserver123",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@ -314,6 +314,66 @@ func TestRecordCreate_forbidenFields(t *testing.T) {
|
||||
End()
|
||||
}
|
||||
|
||||
func TestRecordCreate_xss(t *testing.T) {
|
||||
h := newHelper(t)
|
||||
h.clearRecords()
|
||||
|
||||
h.allow(types.NamespaceRBACResource.AppendWildcard(), "read")
|
||||
h.allow(types.ModuleRBACResource.AppendWildcard(), "read")
|
||||
h.allow(types.ModuleRBACResource.AppendWildcard(), "record.create")
|
||||
h.allow(types.ModuleRBACResource.AppendWildcard(), "record.update")
|
||||
h.allow(types.ModuleRBACResource.AppendWildcard(), "record.read")
|
||||
|
||||
var (
|
||||
ns = h.makeNamespace("some-namespace")
|
||||
mod = h.makeModule(ns, "some-module",
|
||||
&types.ModuleField{
|
||||
Kind: "String",
|
||||
Name: "dummy",
|
||||
},
|
||||
&types.ModuleField{
|
||||
Kind: "String",
|
||||
Name: "dummyRichTextBox",
|
||||
Options: map[string]interface{}{
|
||||
"useRichTextEditor": true,
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
t.Run("create with rich text fields", func(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
|
||||
payload = struct {
|
||||
Response *types.Record
|
||||
}{}
|
||||
|
||||
rec = &types.Record{
|
||||
Values: types.RecordValueSet{
|
||||
&types.RecordValue{Name: "dummyRichTextBox", Value: "<img src=x onerror=alert(11111)>test"},
|
||||
&types.RecordValue{Name: "dummy", Value: "simple-text"},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
h.apiInit().
|
||||
Post(fmt.Sprintf("/namespace/%d/module/%d/record/", ns.ID, mod.ID)).
|
||||
JSON(helpers.JSON(rec)).
|
||||
Expect(t).
|
||||
Status(http.StatusOK).
|
||||
Assert(jsonpath.Present(`$.response.values[? @.name=="dummyRichTextBox"]`)).
|
||||
Assert(jsonpath.Present(`$.response.values[? @.name=="dummy"]`)).
|
||||
Assert(jsonpath.Present(`$.response.values[? @.value=="simple-text"]`)).
|
||||
Assert(jsonpath.Present(`$.response.values[? @.value=="<img src=\"x\">test"]`)).
|
||||
End().
|
||||
JSON(&payload)
|
||||
|
||||
req.NotNil(payload.Response)
|
||||
req.NotZero(payload.Response.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecordCreateWithErrors(t *testing.T) {
|
||||
h := newHelper(t)
|
||||
h.clearRecords()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user