Skip to content

Commit f909660

Browse files
committed
Add middleware.CORSConfig.UnsafeWildcardOriginWithAllowCredentials to make UNSAFE usages of wildcard origin + allow cretentials less likely.
1 parent ef4aea9 commit f909660

File tree

2 files changed

+193
-100
lines changed

2 files changed

+193
-100
lines changed

middleware/cors.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ type (
7979
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
8080
AllowCredentials bool `yaml:"allow_credentials"`
8181

82+
// UnsafeWildcardOriginWithAllowCredentials UNSAFE/INSECURE: allows wildcard '*' origin to be used with AllowCredentials
83+
// flag. In that case we consider any origin allowed and send it back to the client with `Access-Control-Allow-Origin` header.
84+
//
85+
// This is INSECURE and potentially leads to [cross-origin](https://portswigger.net/research/exploiting-cors-misconfigurations-for-bitcoins-and-bounties)
86+
// attacks. See: https://github.com/labstack/echo/issues/2400 for discussion on the subject.
87+
//
88+
// Optional. Default value is false.
89+
UnsafeWildcardOriginWithAllowCredentials bool `yaml:"unsafe_wildcard_origin_with_allow_credentials"`
90+
8291
// ExposeHeaders determines the value of Access-Control-Expose-Headers, which
8392
// defines a list of headers that clients are allowed to access.
8493
//
@@ -203,7 +212,7 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
203212
} else {
204213
// Check allowed origins
205214
for _, o := range config.AllowOrigins {
206-
if o == "*" && config.AllowCredentials {
215+
if o == "*" && config.AllowCredentials && config.UnsafeWildcardOriginWithAllowCredentials {
207216
allowOrigin = origin
208217
break
209218
}

middleware/cors_test.go

Lines changed: 183 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -11,106 +11,190 @@ import (
1111
)
1212

1313
func TestCORS(t *testing.T) {
14-
e := echo.New()
14+
var testCases = []struct {
15+
name string
16+
givenMW echo.MiddlewareFunc
17+
whenMethod string
18+
whenHeaders map[string]string
19+
expectHeaders map[string]string
20+
notExpectHeaders map[string]string
21+
}{
22+
{
23+
name: "ok, wildcard origin",
24+
whenHeaders: map[string]string{echo.HeaderOrigin: "localhost"},
25+
expectHeaders: map[string]string{echo.HeaderAccessControlAllowOrigin: "*"},
26+
},
27+
{
28+
name: "ok, wildcard AllowedOrigin with no Origin header in request",
29+
notExpectHeaders: map[string]string{echo.HeaderAccessControlAllowOrigin: ""},
30+
},
31+
{
32+
name: "ok, specific AllowOrigins and AllowCredentials",
33+
givenMW: CORSWithConfig(CORSConfig{
34+
AllowOrigins: []string{"localhost"},
35+
AllowCredentials: true,
36+
MaxAge: 3600,
37+
}),
38+
whenHeaders: map[string]string{echo.HeaderOrigin: "localhost"},
39+
expectHeaders: map[string]string{
40+
echo.HeaderAccessControlAllowOrigin: "localhost",
41+
echo.HeaderAccessControlAllowCredentials: "true",
42+
},
43+
},
44+
{
45+
name: "ok, preflight request with matching origin for `AllowOrigins`",
46+
givenMW: CORSWithConfig(CORSConfig{
47+
AllowOrigins: []string{"localhost"},
48+
AllowCredentials: true,
49+
MaxAge: 3600,
50+
}),
51+
whenMethod: http.MethodOptions,
52+
whenHeaders: map[string]string{
53+
echo.HeaderOrigin: "localhost",
54+
echo.HeaderContentType: echo.MIMEApplicationJSON,
55+
},
56+
expectHeaders: map[string]string{
57+
echo.HeaderAccessControlAllowOrigin: "localhost",
58+
echo.HeaderAccessControlAllowMethods: "GET,HEAD,PUT,PATCH,POST,DELETE",
59+
echo.HeaderAccessControlAllowCredentials: "true",
60+
echo.HeaderAccessControlMaxAge: "3600",
61+
},
62+
},
63+
{
64+
name: "ok, preflight request with wildcard `AllowOrigins` and `AllowCredentials` true",
65+
givenMW: CORSWithConfig(CORSConfig{
66+
AllowOrigins: []string{"*"},
67+
AllowCredentials: true,
68+
MaxAge: 3600,
69+
}),
70+
whenMethod: http.MethodOptions,
71+
whenHeaders: map[string]string{
72+
echo.HeaderOrigin: "localhost",
73+
echo.HeaderContentType: echo.MIMEApplicationJSON,
74+
},
75+
expectHeaders: map[string]string{
76+
echo.HeaderAccessControlAllowOrigin: "*", // Note: browsers will ignore and complain about responses having `*`
77+
echo.HeaderAccessControlAllowMethods: "GET,HEAD,PUT,PATCH,POST,DELETE",
78+
echo.HeaderAccessControlAllowCredentials: "true",
79+
echo.HeaderAccessControlMaxAge: "3600",
80+
},
81+
},
82+
{
83+
name: "ok, preflight request with wildcard `AllowOrigins` and `AllowCredentials` false",
84+
givenMW: CORSWithConfig(CORSConfig{
85+
AllowOrigins: []string{"*"},
86+
AllowCredentials: false, // important for this testcase
87+
MaxAge: 3600,
88+
}),
89+
whenMethod: http.MethodOptions,
90+
whenHeaders: map[string]string{
91+
echo.HeaderOrigin: "localhost",
92+
echo.HeaderContentType: echo.MIMEApplicationJSON,
93+
},
94+
expectHeaders: map[string]string{
95+
echo.HeaderAccessControlAllowOrigin: "*",
96+
echo.HeaderAccessControlAllowMethods: "GET,HEAD,PUT,PATCH,POST,DELETE",
97+
echo.HeaderAccessControlMaxAge: "3600",
98+
},
99+
notExpectHeaders: map[string]string{
100+
echo.HeaderAccessControlAllowCredentials: "",
101+
},
102+
},
103+
{
104+
name: "ok, INSECURE preflight request with wildcard `AllowOrigins` and `AllowCredentials` true",
105+
givenMW: CORSWithConfig(CORSConfig{
106+
AllowOrigins: []string{"*"},
107+
AllowCredentials: true,
108+
UnsafeWildcardOriginWithAllowCredentials: true, // important for this testcase
109+
MaxAge: 3600,
110+
}),
111+
whenMethod: http.MethodOptions,
112+
whenHeaders: map[string]string{
113+
echo.HeaderOrigin: "localhost",
114+
echo.HeaderContentType: echo.MIMEApplicationJSON,
115+
},
116+
expectHeaders: map[string]string{
117+
echo.HeaderAccessControlAllowOrigin: "localhost", // This could end up as cross-origin attack
118+
echo.HeaderAccessControlAllowMethods: "GET,HEAD,PUT,PATCH,POST,DELETE",
119+
echo.HeaderAccessControlAllowCredentials: "true",
120+
echo.HeaderAccessControlMaxAge: "3600",
121+
},
122+
},
123+
{
124+
name: "ok, preflight request with Access-Control-Request-Headers",
125+
givenMW: CORSWithConfig(CORSConfig{
126+
AllowOrigins: []string{"*"},
127+
}),
128+
whenMethod: http.MethodOptions,
129+
whenHeaders: map[string]string{
130+
echo.HeaderOrigin: "localhost",
131+
echo.HeaderContentType: echo.MIMEApplicationJSON,
132+
echo.HeaderAccessControlRequestHeaders: "Special-Request-Header",
133+
},
134+
expectHeaders: map[string]string{
135+
echo.HeaderAccessControlAllowOrigin: "*",
136+
echo.HeaderAccessControlAllowHeaders: "Special-Request-Header",
137+
echo.HeaderAccessControlAllowMethods: "GET,HEAD,PUT,PATCH,POST,DELETE",
138+
},
139+
},
140+
{
141+
name: "ok, preflight request with `AllowOrigins` which allow all subdomains aaa with *",
142+
givenMW: CORSWithConfig(CORSConfig{
143+
AllowOrigins: []string{"http://*.example.com"},
144+
}),
145+
whenMethod: http.MethodOptions,
146+
whenHeaders: map[string]string{echo.HeaderOrigin: "http://aaa.example.com"},
147+
expectHeaders: map[string]string{echo.HeaderAccessControlAllowOrigin: "http://aaa.example.com"},
148+
},
149+
{
150+
name: "ok, preflight request with `AllowOrigins` which allow all subdomains bbb with *",
151+
givenMW: CORSWithConfig(CORSConfig{
152+
AllowOrigins: []string{"http://*.example.com"},
153+
}),
154+
whenMethod: http.MethodOptions,
155+
whenHeaders: map[string]string{echo.HeaderOrigin: "http://bbb.example.com"},
156+
expectHeaders: map[string]string{echo.HeaderAccessControlAllowOrigin: "http://bbb.example.com"},
157+
},
158+
}
159+
for _, tc := range testCases {
160+
t.Run(tc.name, func(t *testing.T) {
161+
e := echo.New()
162+
163+
mw := CORS()
164+
if tc.givenMW != nil {
165+
mw = tc.givenMW
166+
}
167+
h := mw(func(c echo.Context) error {
168+
return nil
169+
})
15170

16-
// Wildcard origin
17-
req := httptest.NewRequest(http.MethodGet, "/", nil)
18-
rec := httptest.NewRecorder()
19-
c := e.NewContext(req, rec)
20-
h := CORS()(echo.NotFoundHandler)
21-
req.Header.Set(echo.HeaderOrigin, "localhost")
22-
h(c)
23-
assert.Equal(t, "*", rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
24-
25-
// Wildcard AllowedOrigin with no Origin header in request
26-
req = httptest.NewRequest(http.MethodGet, "/", nil)
27-
rec = httptest.NewRecorder()
28-
c = e.NewContext(req, rec)
29-
h = CORS()(echo.NotFoundHandler)
30-
h(c)
31-
assert.NotContains(t, rec.Header(), echo.HeaderAccessControlAllowOrigin)
32-
33-
// Allow origins
34-
req = httptest.NewRequest(http.MethodGet, "/", nil)
35-
rec = httptest.NewRecorder()
36-
c = e.NewContext(req, rec)
37-
h = CORSWithConfig(CORSConfig{
38-
AllowOrigins: []string{"localhost"},
39-
AllowCredentials: true,
40-
MaxAge: 3600,
41-
})(echo.NotFoundHandler)
42-
req.Header.Set(echo.HeaderOrigin, "localhost")
43-
h(c)
44-
assert.Equal(t, "localhost", rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
45-
assert.Equal(t, "true", rec.Header().Get(echo.HeaderAccessControlAllowCredentials))
46-
47-
// Preflight request
48-
req = httptest.NewRequest(http.MethodOptions, "/", nil)
49-
rec = httptest.NewRecorder()
50-
c = e.NewContext(req, rec)
51-
req.Header.Set(echo.HeaderOrigin, "localhost")
52-
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
53-
cors := CORSWithConfig(CORSConfig{
54-
AllowOrigins: []string{"localhost"},
55-
AllowCredentials: true,
56-
MaxAge: 3600,
57-
})
58-
h = cors(echo.NotFoundHandler)
59-
h(c)
60-
assert.Equal(t, "localhost", rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
61-
assert.NotEmpty(t, rec.Header().Get(echo.HeaderAccessControlAllowMethods))
62-
assert.Equal(t, "true", rec.Header().Get(echo.HeaderAccessControlAllowCredentials))
63-
assert.Equal(t, "3600", rec.Header().Get(echo.HeaderAccessControlMaxAge))
64-
65-
// Preflight request with `AllowOrigins` *
66-
req = httptest.NewRequest(http.MethodOptions, "/", nil)
67-
rec = httptest.NewRecorder()
68-
c = e.NewContext(req, rec)
69-
req.Header.Set(echo.HeaderOrigin, "localhost")
70-
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
71-
cors = CORSWithConfig(CORSConfig{
72-
AllowOrigins: []string{"*"},
73-
AllowCredentials: true,
74-
MaxAge: 3600,
75-
})
76-
h = cors(echo.NotFoundHandler)
77-
h(c)
78-
assert.Equal(t, "localhost", rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
79-
assert.NotEmpty(t, rec.Header().Get(echo.HeaderAccessControlAllowMethods))
80-
assert.Equal(t, "true", rec.Header().Get(echo.HeaderAccessControlAllowCredentials))
81-
assert.Equal(t, "3600", rec.Header().Get(echo.HeaderAccessControlMaxAge))
82-
83-
// Preflight request with Access-Control-Request-Headers
84-
req = httptest.NewRequest(http.MethodOptions, "/", nil)
85-
rec = httptest.NewRecorder()
86-
c = e.NewContext(req, rec)
87-
req.Header.Set(echo.HeaderOrigin, "localhost")
88-
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
89-
req.Header.Set(echo.HeaderAccessControlRequestHeaders, "Special-Request-Header")
90-
cors = CORSWithConfig(CORSConfig{
91-
AllowOrigins: []string{"*"},
92-
})
93-
h = cors(echo.NotFoundHandler)
94-
h(c)
95-
assert.Equal(t, "*", rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
96-
assert.Equal(t, "Special-Request-Header", rec.Header().Get(echo.HeaderAccessControlAllowHeaders))
97-
assert.NotEmpty(t, rec.Header().Get(echo.HeaderAccessControlAllowMethods))
98-
99-
// Preflight request with `AllowOrigins` which allow all subdomains with *
100-
req = httptest.NewRequest(http.MethodOptions, "/", nil)
101-
rec = httptest.NewRecorder()
102-
c = e.NewContext(req, rec)
103-
req.Header.Set(echo.HeaderOrigin, "http://aaa.example.com")
104-
cors = CORSWithConfig(CORSConfig{
105-
AllowOrigins: []string{"http://*.example.com"},
106-
})
107-
h = cors(echo.NotFoundHandler)
108-
h(c)
109-
assert.Equal(t, "http://aaa.example.com", rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
110-
111-
req.Header.Set(echo.HeaderOrigin, "http://bbb.example.com")
112-
h(c)
113-
assert.Equal(t, "http://bbb.example.com", rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
171+
method := http.MethodGet
172+
if tc.whenMethod != "" {
173+
method = tc.whenMethod
174+
}
175+
req := httptest.NewRequest(method, "/", nil)
176+
rec := httptest.NewRecorder()
177+
c := e.NewContext(req, rec)
178+
for k, v := range tc.whenHeaders {
179+
req.Header.Set(k, v)
180+
}
181+
182+
err := h(c)
183+
184+
assert.NoError(t, err)
185+
header := rec.Header()
186+
for k, v := range tc.expectHeaders {
187+
assert.Equal(t, v, header.Get(k), "header: `%v` should be `%v`", k, v)
188+
}
189+
for k, v := range tc.notExpectHeaders {
190+
if v == "" {
191+
assert.Len(t, header.Values(k), 0, "header: `%v` should not be set", k)
192+
} else {
193+
assert.NotEqual(t, v, header.Get(k), "header: `%v` should not be `%v`", k, v)
194+
}
195+
}
196+
})
197+
}
114198
}
115199

116200
func Test_allowOriginScheme(t *testing.T) {

0 commit comments

Comments
 (0)