Skip to content

Commit

Permalink
Sanitize text record values
Browse files Browse the repository at this point in the history
  • Loading branch information
tjerman committed Jun 22, 2021
1 parent fb83872 commit 83afe8e
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 7 deletions.
3 changes: 3 additions & 0 deletions compose/service/record.go
Expand Up @@ -50,6 +50,7 @@ type (

recordValuesSanitizer interface {
Run(*types.Module, types.RecordValueSet) types.RecordValueSet
RunXSS(*types.Module, types.RecordValueSet) types.RecordValueSet
}

recordValuesValidator interface {
Expand Down Expand Up @@ -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
}()
Expand Down Expand Up @@ -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
})

Expand Down
52 changes: 46 additions & 6 deletions compose/service/values/sanitizer.go
Expand Up @@ -2,13 +2,16 @@ package values

import (
"fmt"
"github.com/cortezaproject/corteza-server/pkg/expr"
"github.com/cortezaproject/corteza-server/pkg/logger"
"go.uber.org/zap"
"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"
)

Expand Down Expand Up @@ -117,7 +120,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)

Expand All @@ -127,6 +130,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)
Expand All @@ -136,8 +142,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":
Expand All @@ -148,6 +152,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:
Expand Down Expand Up @@ -258,6 +285,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) {
Expand Down
157 changes: 156 additions & 1 deletion compose/service/values/sanitizer_test.go
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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=&#34;https://cortezaproject.org/script.js&#34;&gt;",
},
{
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: "&#39;&#39;;!--&#34;=&amp;{()}",
},
{
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) {
Expand Down
60 changes: 60 additions & 0 deletions tests/compose/record_test.go
Expand Up @@ -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()
Expand Down

0 comments on commit 83afe8e

Please sign in to comment.