Skip to content

Commit 47f3e22

Browse files
committed
Fix path check logic
1 parent 2dc5d14 commit 47f3e22

File tree

4 files changed

+156
-11
lines changed

4 files changed

+156
-11
lines changed

pkg/config/middleware.go

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@ import (
44
"crypto/subtle"
55
"fmt"
66
"net/http"
7+
"slices"
78
"strconv"
8-
"strings"
99
"time"
1010

1111
"github.com/DataDog/datadog-go/statsd"
1212
jwtmiddleware "github.com/auth0/go-jwt-middleware"
1313
jwt "github.com/form3tech-oss/jwt-go"
1414
"github.com/gohttp/pprof"
1515
negronilogrus "github.com/meatballhat/negroni-logrus"
16+
"github.com/openflagr/flagr/pkg/util"
1617
"github.com/phyber/negroni-gzip/gzip"
1718
"github.com/prometheus/client_golang/prometheus"
1819
"github.com/prometheus/client_golang/prometheus/promhttp"
@@ -198,15 +199,13 @@ func (a *jwtAuth) whitelist(req *http.Request) bool {
198199

199200
// If we set to 401 unauthorized, let the client handles the 401 itself
200201
if Config.JWTAuthNoTokenStatusCode == http.StatusUnauthorized {
201-
for _, p := range a.ExactWhitelistPaths {
202-
if p == path {
203-
return true
204-
}
202+
if slices.Contains(a.ExactWhitelistPaths, path) {
203+
return true
205204
}
206205
}
207206

208207
for _, p := range a.PrefixWhitelistPaths {
209-
if p != "" && strings.HasPrefix(path, p) {
208+
if p != "" && util.HasSafePrefix(path, p) {
210209
return true
211210
}
212211
}
@@ -243,14 +242,12 @@ type basicAuth struct {
243242
func (a *basicAuth) whitelist(req *http.Request) bool {
244243
path := req.URL.Path
245244

246-
for _, p := range a.ExactWhitelistPaths {
247-
if p == path {
248-
return true
249-
}
245+
if slices.Contains(a.ExactWhitelistPaths, path) {
246+
return true
250247
}
251248

252249
for _, p := range a.PrefixWhitelistPaths {
253-
if p != "" && strings.HasPrefix(path, p) {
250+
if p != "" && util.HasSafePrefix(path, p) {
254251
return true
255252
}
256253
}

pkg/config/middleware_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,27 @@ func TestJWTAuthMiddlewareWithUnauthorized(t *testing.T) {
314314
})
315315
}
316316
})
317+
318+
t.Run("it will return 401 for some paths", func(t *testing.T) {
319+
Config.JWTAuthEnabled = true
320+
Config.JWTAuthNoTokenStatusCode = http.StatusUnauthorized
321+
defer func() {
322+
Config.JWTAuthEnabled = false
323+
Config.JWTAuthNoTokenStatusCode = http.StatusTemporaryRedirect
324+
}()
325+
326+
testPaths := []string{"/api/v1/flags", "/api/v1/health/..", "/api/v1/admin", "//api/v1/flags", "/..", "/."}
327+
for _, path := range testPaths {
328+
t.Run(fmt.Sprintf("path: %s", path), func(t *testing.T) {
329+
hh := SetupGlobalMiddleware(h)
330+
res := httptest.NewRecorder()
331+
res.Body = new(bytes.Buffer)
332+
req, _ := http.NewRequest("GET", fmt.Sprintf("http://localhost:18000%s", path), nil)
333+
hh.ServeHTTP(res, req)
334+
assert.Equal(t, http.StatusUnauthorized, res.Code)
335+
})
336+
}
337+
})
317338
}
318339

319340
func TestBasicAuthMiddleware(t *testing.T) {

pkg/util/util.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package util
33
import (
44
"fmt"
55
"math"
6+
"path"
67
"regexp"
8+
"strings"
79
"time"
810

911
"github.com/dchest/uniuri"
@@ -43,6 +45,24 @@ func IsSafeValue(s string) (bool, string) {
4345
return true, ""
4446
}
4547

48+
// HasSafePrefix checks if the given string is a safe URL path prefix
49+
func HasSafePrefix(s string, prefix string) bool {
50+
if prefix == "" {
51+
return true
52+
}
53+
54+
// Check for path traversal attempts or suspicious patterns
55+
if s == "." || s == ".." || strings.Contains(s, "..") {
56+
return false
57+
}
58+
59+
// First normalize the path (prefix is controlled by us, no need to clean it)
60+
cleanedS := path.Clean(s)
61+
62+
// Check if the normalized path starts with the prefix
63+
return strings.HasPrefix(cleanedS, prefix)
64+
}
65+
4666
// NewSecureRandomKey creates a new secure random key
4767
func NewSecureRandomKey() string {
4868
return randomKeyPrefix + uniuri.NewLenChars(uniuri.StdLen, randomKeyCharset)

pkg/util/util_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,110 @@ func TestNewSecureRandomKey(t *testing.T) {
191191
ok, _ := IsSafeKey(NewSecureRandomKey())
192192
assert.True(t, ok)
193193
}
194+
195+
func TestHasSafePrefix(t *testing.T) {
196+
tests := []struct {
197+
name string
198+
s string
199+
prefix string
200+
want bool
201+
}{
202+
{
203+
name: "empty prefix always matches",
204+
s: "any/path/here",
205+
prefix: "",
206+
want: true,
207+
},
208+
{
209+
name: "exact prefix match",
210+
s: "api/v1/flags",
211+
prefix: "api/v1",
212+
want: true,
213+
},
214+
{
215+
name: "non-matching prefix",
216+
s: "api/v1/flags",
217+
prefix: "api/v2",
218+
want: false,
219+
},
220+
{
221+
name: "prefix with trailing slash",
222+
s: "api/v1/flags",
223+
prefix: "api/v1/",
224+
want: true,
225+
},
226+
{
227+
name: "path traversal attempt should fail",
228+
s: "../api/v1/flags",
229+
prefix: "api",
230+
want: false,
231+
},
232+
{
233+
name: "good match",
234+
s: "api",
235+
prefix: "api",
236+
want: true,
237+
},
238+
{
239+
name: "path traversal attempt should fail",
240+
s: "..",
241+
prefix: "api",
242+
want: false,
243+
},
244+
{
245+
name: "path traversal attempt should fail",
246+
s: ".",
247+
prefix: "api",
248+
want: false,
249+
},
250+
{
251+
name: "sneaky path traversal should fail",
252+
s: "api/v1/../../secrets",
253+
prefix: "api",
254+
want: false,
255+
},
256+
{
257+
name: "path with dot should be cleaned",
258+
s: "api/./v1/flags",
259+
prefix: "api/v1",
260+
want: true,
261+
},
262+
{
263+
name: "prefix with dot should be cleaned",
264+
s: "api/v1/flags",
265+
prefix: "api/./v1",
266+
want: false,
267+
},
268+
{
269+
name: "multiple slashes should be cleaned",
270+
s: "api///v1////flags",
271+
prefix: "api/v1",
272+
want: true,
273+
},
274+
{
275+
name: "complex traversal attempt should fail",
276+
s: "api/v1/flags/../../../etc/passwd",
277+
prefix: "api",
278+
want: false,
279+
},
280+
{
281+
name: "complex traversal attempt should fail",
282+
s: "api/v1/health/../flags",
283+
prefix: "api/v1/health",
284+
want: false,
285+
},
286+
{
287+
name: "longer path with valid prefix",
288+
s: "api/v1/flags/123/settings",
289+
prefix: "api/v1/flags",
290+
want: true,
291+
},
292+
}
293+
294+
for _, tt := range tests {
295+
t.Run(tt.name, func(t *testing.T) {
296+
got := HasSafePrefix(tt.s, tt.prefix)
297+
assert.Equal(t, tt.want, got, "HasSafePrefix(%v, %v)", tt.s, tt.prefix)
298+
})
299+
}
300+
}

0 commit comments

Comments
 (0)