Skip to content

Commit 0a38a12

Browse files
fail properly while parsing duplicate tags (#1701)
AWS S3 API expects duplicate keys to be rejected, due to our usage of url.ParseQuery() this was skipped since url query parser simply appends duplicate keys as key with multiple values, this is not desirable. Add more input validation and restrictions as per AWS S3 API.
1 parent 43459b0 commit 0a38a12

File tree

3 files changed

+172
-20
lines changed

3 files changed

+172
-20
lines changed

.github/workflows/go.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
runs-on: ${{ matrix.os }}
1818
strategy:
1919
matrix:
20-
go-version: [1.18.x, 1.19.x]
20+
go-version: [1.17.x, 1.18.x, 1.19.x]
2121
os: [ubuntu-latest]
2222
steps:
2323
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}

pkg/tags/tags.go

+82-19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
2-
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
2+
* MinIO Go Library for Amazon S3 Compatible Cloud Storage
3+
* Copyright 2020-2022 MinIO, Inc.
34
*
45
* Licensed under the Apache License, Version 2.0 (the "License");
56
* you may not use this file except in compliance with the License.
@@ -20,6 +21,8 @@ import (
2021
"encoding/xml"
2122
"io"
2223
"net/url"
24+
"regexp"
25+
"sort"
2326
"strings"
2427
"unicode/utf8"
2528
)
@@ -63,17 +66,28 @@ const (
6366
maxTagCount = 50
6467
)
6568

69+
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#tag-restrictions
70+
// borrowed from this article and also testing various ASCII characters following regex
71+
// is supported by AWS S3 for both tags and values.
72+
var validTagKeyValue = regexp.MustCompile(`^[a-zA-Z0-9-+\-._:/@]+$`)
73+
6674
func checkKey(key string) error {
67-
if len(key) == 0 || utf8.RuneCountInString(key) > maxKeyLength || strings.Contains(key, "&") {
75+
if len(key) == 0 {
76+
return errInvalidTagKey
77+
}
78+
79+
if utf8.RuneCountInString(key) > maxKeyLength || !validTagKeyValue.MatchString(key) {
6880
return errInvalidTagKey
6981
}
7082

7183
return nil
7284
}
7385

7486
func checkValue(value string) error {
75-
if utf8.RuneCountInString(value) > maxValueLength || strings.Contains(value, "&") {
76-
return errInvalidTagValue
87+
if value != "" {
88+
if utf8.RuneCountInString(value) > maxValueLength || !validTagKeyValue.MatchString(value) {
89+
return errInvalidTagValue
90+
}
7791
}
7892

7993
return nil
@@ -136,11 +150,26 @@ type tagSet struct {
136150
}
137151

138152
func (tags tagSet) String() string {
139-
vals := make(url.Values)
140-
for key, value := range tags.tagMap {
141-
vals.Set(key, value)
153+
if len(tags.tagMap) == 0 {
154+
return ""
142155
}
143-
return vals.Encode()
156+
var buf strings.Builder
157+
keys := make([]string, 0, len(tags.tagMap))
158+
for k := range tags.tagMap {
159+
keys = append(keys, k)
160+
}
161+
sort.Strings(keys)
162+
for _, k := range keys {
163+
keyEscaped := url.QueryEscape(k)
164+
valueEscaped := url.QueryEscape(tags.tagMap[k])
165+
if buf.Len() > 0 {
166+
buf.WriteByte('&')
167+
}
168+
buf.WriteString(keyEscaped)
169+
buf.WriteByte('=')
170+
buf.WriteString(valueEscaped)
171+
}
172+
return buf.String()
144173
}
145174

146175
func (tags *tagSet) remove(key string) {
@@ -175,7 +204,7 @@ func (tags *tagSet) set(key, value string, failOnExist bool) error {
175204
}
176205

177206
func (tags tagSet) toMap() map[string]string {
178-
m := make(map[string]string)
207+
m := make(map[string]string, len(tags.tagMap))
179208
for key, value := range tags.tagMap {
180209
m[key] = value
181210
}
@@ -188,6 +217,7 @@ func (tags tagSet) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
188217
Tags []Tag `xml:"Tag"`
189218
}{}
190219

220+
tagList.Tags = make([]Tag, 0, len(tags.tagMap))
191221
for key, value := range tags.tagMap {
192222
tagList.Tags = append(tagList.Tags, Tag{key, value})
193223
}
@@ -213,7 +243,7 @@ func (tags *tagSet) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
213243
return errTooManyTags
214244
}
215245

216-
m := map[string]string{}
246+
m := make(map[string]string, len(tagList.Tags))
217247
for _, tag := range tagList.Tags {
218248
if _, found := m[tag.Key]; found {
219249
return errDuplicateTagKey
@@ -311,25 +341,58 @@ func ParseObjectXML(reader io.Reader) (*Tags, error) {
311341
return unmarshalXML(reader, true)
312342
}
313343

344+
// stringsCut slices s around the first instance of sep,
345+
// returning the text before and after sep.
346+
// The found result reports whether sep appears in s.
347+
// If sep does not appear in s, cut returns s, "", false.
348+
func stringsCut(s, sep string) (before, after string, found bool) {
349+
if i := strings.Index(s, sep); i >= 0 {
350+
return s[:i], s[i+len(sep):], true
351+
}
352+
return s, "", false
353+
}
354+
355+
func (tags *tagSet) parseTags(tgs string) (err error) {
356+
for tgs != "" {
357+
var key string
358+
key, tgs, _ = stringsCut(tgs, "&")
359+
if key == "" {
360+
continue
361+
}
362+
key, value, _ := stringsCut(key, "=")
363+
key, err1 := url.QueryUnescape(key)
364+
if err1 != nil {
365+
if err == nil {
366+
err = err1
367+
}
368+
continue
369+
}
370+
value, err1 = url.QueryUnescape(value)
371+
if err1 != nil {
372+
if err == nil {
373+
err = err1
374+
}
375+
continue
376+
}
377+
if err = tags.set(key, value, true); err != nil {
378+
return err
379+
}
380+
}
381+
return err
382+
}
383+
314384
// Parse decodes HTTP query formatted string into tags which is limited by isObject.
315385
// A query formatted string is like "key1=value1&key2=value2".
316386
func Parse(s string, isObject bool) (*Tags, error) {
317-
values, err := url.ParseQuery(s)
318-
if err != nil {
319-
return nil, err
320-
}
321-
322387
tagging := &Tags{
323388
TagSet: &tagSet{
324389
tagMap: make(map[string]string),
325390
isObject: isObject,
326391
},
327392
}
328393

329-
for key := range values {
330-
if err := tagging.TagSet.set(key, values.Get(key), true); err != nil {
331-
return nil, err
332-
}
394+
if err := tagging.TagSet.parseTags(s); err != nil {
395+
return nil, err
333396
}
334397

335398
return tagging, nil

pkg/tags/tags_test.go

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* MinIO Go Library for Amazon S3 Compatible Cloud Storage
3+
* Copyright 2022 MinIO, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package tags
19+
20+
import (
21+
"fmt"
22+
"testing"
23+
)
24+
25+
func TestParseTags(t *testing.T) {
26+
testCases := []struct {
27+
tags string
28+
expectedErr bool
29+
}{
30+
{
31+
"key1=value1&key2=value2",
32+
false,
33+
},
34+
{
35+
fmt.Sprintf("%0128d=%0256d", 1, 1),
36+
false,
37+
},
38+
// Failure cases - https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#tag-restrictions
39+
{
40+
"key1=value1&key1=value2",
41+
true,
42+
},
43+
{
44+
"key$=value1",
45+
true,
46+
},
47+
{
48+
"key1=value$",
49+
true,
50+
},
51+
{
52+
fmt.Sprintf("%0128d=%0257d", 1, 1),
53+
true,
54+
},
55+
{
56+
fmt.Sprintf("%0129d=%0256d", 1, 1),
57+
true,
58+
},
59+
{
60+
fmt.Sprintf("%0129d=%0257d", 1, 1),
61+
true,
62+
},
63+
}
64+
65+
for _, testCase := range testCases {
66+
testCase := testCase
67+
t.Run(testCase.tags, func(t *testing.T) {
68+
tt, err := ParseObjectTags(testCase.tags)
69+
if !testCase.expectedErr && err != nil {
70+
t.Errorf("Expected success but failed with %v", err)
71+
}
72+
if testCase.expectedErr && err == nil {
73+
t.Error("Expected failure but found success")
74+
}
75+
if err == nil {
76+
t.Logf("%s", tt)
77+
}
78+
})
79+
}
80+
}
81+
82+
func BenchmarkParseTags(b *testing.B) {
83+
b.ResetTimer()
84+
b.ReportAllocs()
85+
86+
for i := 0; i < b.N; i++ {
87+
ParseObjectTags("key1=value1&key2=value2")
88+
}
89+
}

0 commit comments

Comments
 (0)