diff --git a/compose/service/record.go b/compose/service/record.go
index 46f12212ec..b3eac94efa 100644
--- a/compose/service/record.go
+++ b/compose/service/record.go
@@ -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
})
diff --git a/compose/service/values/sanitizer.go b/compose/service/values/sanitizer.go
index 21c3601702..f86d567fa6 100644
--- a/compose/service/values/sanitizer.go
+++ b/compose/service/values/sanitizer.go
@@ -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"
)
@@ -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)
@@ -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)
@@ -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":
@@ -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:
@@ -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) {
diff --git a/compose/service/values/sanitizer_test.go b/compose/service/values/sanitizer_test.go
index 7491c682b7..036e6a8bc6 100644
--- a/compose/service/values/sanitizer_test.go
+++ b/compose/service/values/sanitizer_test.go
@@ -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: "Title here",
+ output: "Title here",
+ },
+ {
+ name: "string escaping; html a.href with javascript alert",
+ kind: "String",
+ options: map[string]interface{}{},
+ input: `XSS`,
+ output: "XSS",
+ },
+ {
+ name: "string escaping; a.href with javascript",
+ kind: "String",
+ options: map[string]interface{}{},
+ input: `XSS`,
+ output: "XSS",
+ },
+ {
+ name: "string escaping; script with script",
+ kind: "String",
+ options: map[string]interface{}{},
+ input: `pt src="https://cortezaproject.org/script.js">`,
+ output: "pt src="https://cortezaproject.org/script.js">",
+ },
+ {
+ name: "string escaping; script with a",
+ kind: "String",
+ options: map[string]interface{}{},
+ input: ``,
+ output: "",
+ },
+ {
+ name: "string escaping; meta with script",
+ kind: "String",
+ options: map[string]interface{}{},
+ input: ``,
+ output: "",
+ },
+ {
+ name: "string escaping; object",
+ kind: "String",
+ options: map[string]interface{}{},
+ input: ``,
+ output: "",
+ },
+ {
+ name: "string escaping; base href",
+ kind: "String",
+ options: map[string]interface{}{},
+ input: `