Skip to content

Commit 2166488

Browse files
authored
adding option for xss and date sanitization (#9)
* adding option for xss and date sanitization * updated docs
1 parent b34fa63 commit 2166488

File tree

7 files changed

+304
-10
lines changed

7 files changed

+304
-10
lines changed

README.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,51 @@ func main() {
3838
}
3939
```
4040

41+
## Available options
42+
43+
### Tag Name
44+
45+
Default: `san`
46+
47+
Use this option to tell the sanitizer to use another tag name instead of "san".
48+
49+
```go
50+
s := sanitizer.New(sanitizer.OptionTagName{
51+
Value: "mytag",
52+
})
53+
```
54+
55+
### Date Format
56+
57+
Default: `Input = []`, `Output = ""`, and `KeepFormat = false`.
58+
59+
Use this option to specify which date format we should use.
60+
61+
The `Input` field tells us which date formats we can accept (such as RFC3339 and RFC3339Nano).
62+
63+
The `KeepFormat` field tells us if we should keep the date format unchanged, or if we want to force them into another format.
64+
65+
The `Output` field tells us which format we should use for the output if `KeepFormat` is set to false.
66+
67+
If a date can not be parsed by the formats specified in the `Input` field, the field will be converted into an empty string.
68+
69+
Example:
70+
- If `Input = [RFC1123, RFC3339Nano]`, `KeepFormat: false`, and `Output = RFC3339`, we will accept dates in the RFC1123 and RFC3339Nano formats and convert them to RFC3339 format. Any other formats will be converted into an empty string.
71+
- If `Input = [RFC1123, RFC3339Nano]` and `KeepFormat: true`, we will accept dates in the RFC1123 and RFC3339Nano formats and keep them in the same format. Any other formats will be converted into an empty string.
72+
- If `Input = []`, the field will be converted into an empty string, since there are no allowed input formats.
73+
74+
```go
75+
s := sanitizer.New(sanitizer.OptionDateFormat{
76+
Input: []string{
77+
time.RFC3339,
78+
time.RFC3339Nano,
79+
},
80+
KeepFormat: false,
81+
Output: time.RFC1123,
82+
})
83+
```
84+
85+
4186
## Available tags
4287

4388
### string
@@ -49,8 +94,10 @@ func main() {
4994
1. **title** - First character of every word is changed to uppercase, the rest to lowercase
5095
1. **cap** - Only the first letter of the string will be changed to uppercase, the rest to lowercase
5196
1. **def=`<n>`** (only available for pointers) - Sets a default `<n>` value in case the pointer is `nil`
97+
1. **xss** - Will remove brackets such as <>[](){} and the characters !=? from the string
98+
1. **date** - Will parse the string using the input formats provided in the options and print it using the output format provided in the options. If the string can not be parsed, it will be left empty.
5299

53-
The order of precedence will be: **trim** -> **max** -> **lower**
100+
The order of precedence will be: **xss** -> **trim** -> **date** -> **max** -> **lower** -> **upper** -> **title** -> **cap**
54101

55102

56103
### int, uint, and float

options.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package sanitize
33
// Option represents an optional setting for the sanitizer library
44
type Option interface {
55
id() string
6-
value() string
6+
value() interface{}
77
}
88

99
// OptionTagName allows users to use custom tag names for the structs
@@ -19,6 +19,27 @@ func (o OptionTagName) id() string {
1919
return optionTagNameID
2020
}
2121

22-
func (o OptionTagName) value() string {
22+
func (o OptionTagName) value() interface{} {
2323
return o.Value
2424
}
25+
26+
// OptionDateFormat allows users to specify what date formats are accepted
27+
// as input and what is expected as output. You can choose to force the date
28+
// to be parsed in a different format, or keep the original format
29+
type OptionDateFormat struct {
30+
Input []string
31+
KeepFormat bool
32+
Output string
33+
}
34+
35+
var _ Option = OptionDateFormat{}
36+
37+
const optionDateFormatID = "date-format"
38+
39+
func (o OptionDateFormat) id() string {
40+
return optionDateFormatID
41+
}
42+
43+
func (o OptionDateFormat) value() interface{} {
44+
return o
45+
}

options_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ func (o unknownOption) id() string {
1313
return "strangetag!"
1414
}
1515

16-
func (o unknownOption) value() string {
16+
func (o unknownOption) value() interface{} {
1717
return "very strange indeed"
1818
}
1919

sanitize.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ const DefaultTagName = "san"
1313

1414
// Sanitizer intance
1515
type Sanitizer struct {
16-
tagName string
16+
tagName string
17+
dateInput []string
18+
dateKeepFormat bool
19+
dateOutput string
1720
}
1821

1922
// New sanitizer instance
@@ -24,13 +27,18 @@ func New(options ...Option) (*Sanitizer, error) {
2427
for _, o := range options {
2528
switch o.id() {
2629
case optionTagNameID:
27-
v := o.value()
30+
v := o.value().(string)
2831
if len(v) < 1 || len(v) > 10 {
2932
return nil, fmt.Errorf("tag name %q must be between 1 and 10 characters", v)
3033
}
3134
s.tagName = v
35+
case optionDateFormatID:
36+
v := o.value().(OptionDateFormat)
37+
s.dateInput = v.Input
38+
s.dateKeepFormat = v.KeepFormat
39+
s.dateOutput = v.Output
3240
default:
33-
return nil, fmt.Errorf("tag name %q is not valid", o.value())
41+
return nil, fmt.Errorf("option %q is not valid", o.id())
3442
}
3543
}
3644
return s, nil

sanitize_test.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ package sanitize
33
import (
44
"reflect"
55
"testing"
6+
"time"
67
)
78

8-
func Test_Sanitize_Simple(t *testing.T) {
9+
func Test_Sanitize_CodeSample(t *testing.T) {
910
type Dog struct {
1011
Name string `san:"max=5,trim,lower"`
1112
Breed *string `san:"def=unknown"`
@@ -44,6 +45,41 @@ func Test_Sanitize_Simple(t *testing.T) {
4445
}
4546
}
4647

48+
func Test_Sanitize_Options(t *testing.T) {
49+
type Dog struct {
50+
Name string `abcde:"max=5,trim,lower"`
51+
Birthday string `abcde:"date"`
52+
PersonalWebsite string `abcde:"xss,trim"`
53+
}
54+
55+
now := time.Now()
56+
57+
d := Dog{
58+
Name: "Borky Borkins",
59+
Birthday: now.Format(time.RFC3339),
60+
PersonalWebsite: "<html>[head]1=1?;{/head}(/html)",
61+
}
62+
63+
expected := Dog{
64+
Name: "borky",
65+
Birthday: now.Format(time.RFC850),
66+
PersonalWebsite: "html head 1 1 /head /html",
67+
}
68+
69+
s, _ := New(
70+
OptionTagName{Value: "abcde"},
71+
OptionDateFormat{
72+
Input: []string{time.RFC3339},
73+
Output: time.RFC850,
74+
},
75+
)
76+
s.Sanitize(&d)
77+
78+
if !reflect.DeepEqual(d, expected) {
79+
t.Errorf("Sanitize() - got %+v but wanted %+v", d, expected)
80+
}
81+
}
82+
4783
func Test_Sanitize(t *testing.T) {
4884

4985
type TestStruct struct {

string.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package sanitize
22

33
import (
44
"reflect"
5+
"regexp"
56
"strconv"
67
"strings"
8+
"time"
79
)
810

911
// sanitizeStrField sanitizes a string field. Requires the whole
@@ -46,7 +48,14 @@ func sanitizeStrField(s Sanitizer, structValue reflect.Value, idx int) error {
4648
field = field.Elem()
4749
}
4850

49-
// Trim must happen first, no matter what other components there are.
51+
// Let's strip out invalid characters before anything else
52+
if _, ok := tags["xss"]; ok {
53+
oldStr := field.String()
54+
field.SetString(xss(oldStr))
55+
}
56+
57+
// Trim must happen before the other tags, no matter what other
58+
// components there are.
5059
if _, ok := tags["trim"]; ok {
5160
// Ignore value of this component, we don't care *how* to trim,
5261
// we just trim.
@@ -55,6 +64,10 @@ func sanitizeStrField(s Sanitizer, structValue reflect.Value, idx int) error {
5564
}
5665

5766
// Apply rest of transforms
67+
if _, ok := tags["date"]; ok {
68+
oldStr := field.String()
69+
field.SetString(date(s.dateInput, s.dateKeepFormat, s.dateOutput, oldStr))
70+
}
5871
if _, ok := tags["max"]; ok {
5972
max, err := strconv.ParseInt(tags["max"], 10, 32)
6073
if err != nil {
@@ -65,7 +78,6 @@ func sanitizeStrField(s Sanitizer, structValue reflect.Value, idx int) error {
6578
field.SetString(oldStr[0:max])
6679
}
6780
}
68-
6981
if _, ok := tags["lower"]; ok {
7082
oldStr := field.String()
7183
field.SetString(strings.ToLower(oldStr))
@@ -132,3 +144,27 @@ func toCap(s string) string {
132144
}
133145
return string(b)
134146
}
147+
148+
var replaceWhitespaces = regexp.MustCompile(`\s\s+`)
149+
var blacklistStripping = regexp.MustCompile(`[\p{Me}\p{C}<>=;(){}\[\]?]`)
150+
151+
func xss(s string) string {
152+
s = blacklistStripping.ReplaceAllString(s, " ")
153+
s = replaceWhitespaces.ReplaceAllString(s, " ")
154+
return s
155+
}
156+
157+
func date(in []string, keepFormat bool, out, v string) string {
158+
for _, f := range in {
159+
t, err := time.Parse(f, v)
160+
if err != nil {
161+
continue
162+
}
163+
outf := f
164+
if !keepFormat {
165+
outf = out
166+
}
167+
return t.Format(outf)
168+
}
169+
return ""
170+
}

0 commit comments

Comments
 (0)