Skip to content

Commit 507b158

Browse files
committed
avoid urldecoding of unsafe chars (slashes): now slashes can be registered as a part of path segment
1 parent f5cb29a commit 507b158

File tree

3 files changed

+60
-22
lines changed

3 files changed

+60
-22
lines changed

internal/buffer/buffer.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ func New(initialSize, maxSize int) *Buffer {
1515
}
1616
}
1717

18+
// AppendBytes is a variadic version of Append.
19+
func (b *Buffer) AppendBytes(bytes ...byte) (ok bool) {
20+
return b.Append(bytes)
21+
}
22+
1823
// Append writes data, checking whether the new amount of elements (bytes) doesn't exceed the
1924
// limit, otherwise discarding the data and returning false.
2025
func (b *Buffer) Append(elements []byte) (ok bool) {

internal/protocol/http1/parser.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,25 +150,29 @@ path:
150150
for i := 0; i < len(data); i++ {
151151
switch char := data[i]; char {
152152
case '%':
153-
if !requestLine.Append(data[checkpoint:i]) {
154-
return true, nil, status.ErrURITooLong
155-
}
156-
157153
if len(data[i+1:]) >= 2 {
158154
// fast path
159155
c := (hexconv.Halfbyte[data[i+1]] << 4) | hexconv.Halfbyte[data[i+2]]
156+
if isUnsafeChar(c) {
157+
i += 2
158+
continue
159+
}
160160
if isProhibitedChar(c) {
161161
return true, nil, status.ErrBadRequest
162162
}
163163

164-
if !requestLine.AppendByte(c) {
164+
if !requestLine.Append(data[checkpoint:i]) || !requestLine.AppendByte(c) {
165165
return true, nil, status.ErrURITooLong
166166
}
167167

168168
i += 2
169169
checkpoint = i + 1
170170
} else {
171171
// slow path
172+
if !requestLine.Append(data[checkpoint:i]) {
173+
return true, nil, status.ErrURITooLong
174+
}
175+
172176
data = data[i+1:]
173177
goto pathDecode1Char
174178
}
@@ -229,6 +233,13 @@ pathDecode2Char:
229233
}
230234

231235
char := (hexconv.Halfbyte[p.urlEncodedChar] << 4) | hexconv.Halfbyte[data[0]]
236+
if isUnsafeChar(char) {
237+
if !requestLine.AppendBytes('%', p.urlEncodedChar) {
238+
return true, nil, status.ErrURITooLong
239+
}
240+
241+
goto path
242+
}
232243
if isProhibitedChar(char) {
233244
return true, nil, status.ErrBadRequest
234245
}
@@ -713,6 +724,11 @@ func stripCR(b []byte) []byte {
713724
return b
714725
}
715726

727+
func isUnsafeChar(c byte) bool {
728+
// slash is the only symbol that influences path semantics if carelessly decoded
729+
return c == '/'
730+
}
731+
716732
func isProhibitedChar(c byte) bool {
717733
return c < 0x20 || c > 0x7e
718734
}

internal/protocol/http1/parser_test.go

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -151,21 +151,19 @@ func TestParser(t *testing.T) {
151151
}
152152

153153
compareRequests(t, wanted, request)
154-
request.Reset()
155154
})
156155

157-
t.Run("simple GET with leading CRLF", func(t *testing.T) {
156+
t.Run("leading CRLF", func(t *testing.T) {
158157
raw := "\r\n\r\nGET / HTTP/1.1\r\n\r\n"
159-
parser, request := getParser(cfg)
158+
parser, _ := getParser(cfg)
160159
done, extra, err := parser.Parse([]byte(raw))
161160
// unfortunately, we don't support this. Such clients must die.
162161
require.Error(t, err, status.ErrBadRequest.Error())
163162
require.True(t, done)
164163
require.Empty(t, extra)
165-
request.Reset()
166164
})
167165

168-
t.Run("normal GET", func(t *testing.T) {
166+
t.Run("GET with headers", func(t *testing.T) {
169167
raw := "GET / HTTP/1.1\r\nHello: World!\r\nEaster: Egg\r\n\r\n"
170168
parser, request := getParser(cfg)
171169
done, extra, err := parser.Parse([]byte(raw))
@@ -183,7 +181,6 @@ func TestParser(t *testing.T) {
183181
}
184182

185183
compareRequests(t, wanted, request)
186-
request.Reset()
187184
})
188185

189186
t.Run("multiple header values", func(t *testing.T) {
@@ -204,7 +201,6 @@ func TestParser(t *testing.T) {
204201
}
205202

206203
compareRequests(t, wanted, request)
207-
request.Reset()
208204
})
209205

210206
t.Run("only lf", func(t *testing.T) {
@@ -225,7 +221,6 @@ func TestParser(t *testing.T) {
225221
}
226222

227223
compareRequests(t, wanted, request)
228-
request.Reset()
229224
})
230225

231226
t.Run("fuzz GET", func(t *testing.T) {
@@ -268,7 +263,6 @@ func TestParser(t *testing.T) {
268263
}
269264

270265
compareRequests(t, wanted, request)
271-
request.Reset()
272266
})
273267

274268
t.Run("content length", func(t *testing.T) {
@@ -279,6 +273,7 @@ func TestParser(t *testing.T) {
279273
require.True(t, done)
280274
require.Equal(t, "Hello, world!", string(extra))
281275
require.Equal(t, 13, request.ContentLength)
276+
require.Equal(t, "13", request.Headers.Value("content-length"))
282277
request.Reset()
283278

284279
raw = "GET / HTTP/1.1\r\nContent-Length: 13\r\nHi-Hi: ha-ha\r\n\r\nHello, world!"
@@ -287,9 +282,8 @@ func TestParser(t *testing.T) {
287282
require.True(t, done)
288283
require.Equal(t, "Hello, world!", string(extra))
289284
require.Equal(t, 13, request.ContentLength)
290-
require.True(t, request.Headers.Has("hi-hi"))
285+
require.Equal(t, "13", request.Headers.Value("content-length"))
291286
require.Equal(t, "ha-ha", request.Headers.Value("hi-hi"))
292-
request.Reset()
293287
})
294288

295289
t.Run("connection", func(t *testing.T) {
@@ -300,7 +294,6 @@ func TestParser(t *testing.T) {
300294
require.True(t, done)
301295
require.Empty(t, string(extra))
302296
require.Equal(t, "Keep-Alive", request.Connection)
303-
request.Reset()
304297
})
305298

306299
t.Run("Transfer-Encoding and Content-Encoding", func(t *testing.T) {
@@ -313,22 +306,26 @@ func TestParser(t *testing.T) {
313306
require.Equal(t, []string{"chunked"}, request.TransferEncoding)
314307
require.True(t, request.Chunked)
315308
require.Equal(t, []string{"gzip", "deflate"}, request.ContentEncoding)
316-
request.Reset()
317309
})
318310

319311
t.Run("urldecode", func(t *testing.T) {
320-
parsePath := func(encodedPath string) (string, error) {
321-
raw := fmt.Sprintf("GET %s ", encodedPath)
312+
parseRequestLine := func(path string) (*http.Request, error) {
313+
raw := fmt.Sprintf("GET %s ", path)
322314
parser, request := getParser(cfg)
323315

324316
for i := 0; i < len(raw); i++ {
325317
_, _, err := parser.Parse([]byte{raw[i]})
326318
if err != nil {
327-
return "", err
319+
return request, err
328320
}
329321
}
330322

331-
return request.Path, nil
323+
return request, nil
324+
}
325+
326+
parsePath := func(path string) (string, error) {
327+
request, err := parseRequestLine(path)
328+
return request.Path, err
332329
}
333330

334331
t.Run("path", func(t *testing.T) {
@@ -342,6 +339,26 @@ func TestParser(t *testing.T) {
342339
require.EqualError(t, err, status.ErrBadRequest.Error())
343340
})
344341

342+
t.Run("unsafe", func(t *testing.T) {
343+
test := func(t *testing.T, request *http.Request) {
344+
require.Equal(t, "/ foo%2fbar#?", request.Path)
345+
require.Equal(t, "bar", request.Params.Value("foo+="))
346+
}
347+
348+
t.Run("slowpath", func(t *testing.T) {
349+
request, err := parseRequestLine("/%20foo%2fbar%23%3f?foo%2b%3d=bar")
350+
require.NoError(t, err)
351+
test(t, request)
352+
})
353+
354+
t.Run("fastpath", func(t *testing.T) {
355+
parser, request := getParser(config.Default())
356+
_, _, err := parser.Parse([]byte("GET /%20foo%2fbar%23%3f?foo%2b%3d=bar "))
357+
require.NoError(t, err)
358+
test(t, request)
359+
})
360+
})
361+
345362
t.Run("params", func(t *testing.T) {
346363
parseParams := func(params ...string) (http.Params, error) {
347364
parser, request := getParser(config.Default())

0 commit comments

Comments
 (0)