WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 9b59427

Browse files
authored
Merge pull request from GHSA-xrmp-4542-q746
Advisory fix 1
2 parents 091c543 + 8860b4c commit 9b59427

File tree

6 files changed

+180
-61
lines changed

6 files changed

+180
-61
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,16 @@ Changelog
22
=========
33

44
## HEAD
5+
6+
## v1.1.5 2019-07-23
7+
* Security fixes / SSRF
8+
* Fix: Ensure non-GET/HEAD request does not send outbound request (#35)
9+
* Fix: Validate redirect urls the same as initial urls (#35)
510
* Split out exception for missing content types (#32)
611
* Prometheus compatible metrics endpoint added (#34)
12+
* Disabled credential/userinfo (`user:pass@` style) type urls by default.
13+
Added cli flag (`--allow-credential-urls`) to retain prior behavior (which
14+
allows them).
715

816
## v1.1.4 2019-02-26
917

cmd/go-camo/main.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ func main() {
7070
DisableKeepAlivesFE bool `long:"no-fk" description:"Disable frontend http keep-alive support"`
7171
DisableKeepAlivesBE bool `long:"no-bk" description:"Disable backend http keep-alive support"`
7272
AllowContentVideo bool `long:"allow-content-video" description:"Additionally allow 'video/*' content"`
73+
AllowCredetialURLs bool `long:"allow-credential-urls" description:"Allow urls to contain user/pass credentials"`
7374
Verbose bool `short:"v" long:"verbose" description:"Show verbose (debug) log level output"`
7475
ServerName string `long:"server-name" default:"go-camo" description:"Value to use for the HTTP server field"`
7576
ExposeServerVersion bool `long:"expose-server-version" description:"Include the server version in the HTTP server response header"`
@@ -142,6 +143,7 @@ func main() {
142143

143144
// other options
144145
config.EnableXFwdFor = opts.EnableXFwdFor
146+
config.AllowCredetialURLs = opts.AllowCredetialURLs
145147

146148
// additional content types to allow
147149
config.AllowContentVideo = opts.AllowContentVideo
@@ -205,24 +207,25 @@ func main() {
205207
CamoHandler: proxy,
206208
}
207209

208-
if opts.Metrics {
209-
mlog.Printf("Enabling metrics at /metrics")
210-
http.Handle("/metrics", promhttp.Handler())
211-
}
212-
213210
if opts.Stats {
214211
ps := &stats.ProxyStats{}
215212
proxy.SetMetricsCollector(ps)
216213
mlog.Printf("Enabling stats at /status")
217214
dumbrouter.StatsHandler = stats.Handler(ps)
218215
}
219216

220-
// Wrap the dumb router in instrumentation.
221-
instrumentedRouter := promhttp.InstrumentHandlerDuration(responseDuration,
222-
promhttp.InstrumentHandlerResponseSize(responseSize, dumbrouter),
223-
)
217+
var router http.Handler = dumbrouter
218+
219+
if opts.Metrics {
220+
mlog.Printf("Enabling metrics at /metrics")
221+
http.Handle("/metrics", promhttp.Handler())
222+
// Wrap the dumb router in instrumentation.
223+
router = promhttp.InstrumentHandlerDuration(responseDuration,
224+
promhttp.InstrumentHandlerResponseSize(responseSize, dumbrouter),
225+
)
226+
}
224227

225-
http.Handle("/", instrumentedRouter)
228+
http.Handle("/", router)
226229

227230
if opts.BindAddress != "" {
228231
mlog.Printf("Starting server on: %s", opts.BindAddress)

pkg/camo/proxy.go

Lines changed: 69 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package camo
88

99
import (
1010
"errors"
11+
"fmt"
1112
"io"
1213
"net"
1314
"net/http"
@@ -45,6 +46,8 @@ type Config struct {
4546
EnableXFwdFor bool
4647
// additional content types to allow
4748
AllowContentVideo bool
49+
// allow URLs to contain user/pass credentials
50+
AllowCredetialURLs bool
4851
// no ip filtering (test mode)
4952
noIPFiltering bool
5053
}
@@ -112,40 +115,12 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, req *http.Request) {
112115
return
113116
}
114117

115-
uHostname := strings.ToLower(u.Hostname())
116-
if uHostname == "" || localhostRegex.MatchString(uHostname) {
117-
http.Error(w, "Bad url host", http.StatusNotFound)
118+
err = p.checkURL(u)
119+
if err != nil {
120+
http.Error(w, err.Error(), http.StatusNotFound)
118121
return
119122
}
120123

121-
// filtering
122-
if !p.config.noIPFiltering {
123-
// if allowList is set, require match
124-
for _, rgx := range p.allowList {
125-
if rgx.MatchString(uHostname) {
126-
http.Error(w, "Allowlist host failure", http.StatusNotFound)
127-
return
128-
}
129-
}
130-
131-
// filter out rejected networks
132-
if ip := net.ParseIP(uHostname); ip != nil {
133-
if isRejectedIP(ip) {
134-
http.Error(w, "Denylist host failure", http.StatusNotFound)
135-
return
136-
}
137-
} else {
138-
if ips, err := net.LookupIP(uHostname); err == nil {
139-
for _, ip := range ips {
140-
if isRejectedIP(ip) {
141-
http.Error(w, "Denylist host failure", http.StatusNotFound)
142-
return
143-
}
144-
}
145-
}
146-
}
147-
}
148-
149124
nreq, err := http.NewRequest(req.Method, sURL, nil)
150125
if err != nil {
151126
mlog.Debugm("could not create NewRequest", mlog.Map{"err": err})
@@ -203,6 +178,10 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, req *http.Request) {
203178
http.Error(w, "Error Fetching Resource", http.StatusGatewayTimeout)
204179
} else if strings.Contains(errString, "use of closed") {
205180
http.Error(w, "Error Fetching Resource", http.StatusBadGateway)
181+
} else if strings.Contains(errString, "BadRedirect:") {
182+
// Got a bad redirect
183+
mlog.Debugm("response from upstream", mlog.Map{"err": err})
184+
http.Error(w, "Error Fetching Resource", http.StatusNotFound)
206185
} else {
207186
// some other error. call it a not found (camo compliant)
208187
http.Error(w, "Error Fetching Resource", http.StatusNotFound)
@@ -296,6 +275,46 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, req *http.Request) {
296275
mlog.Debugm("response to client", mlog.Map{"resp": w})
297276
}
298277

278+
func (p *Proxy) checkURL(reqURL *url.URL) error {
279+
// reject localhost urls
280+
uHostname := strings.ToLower(reqURL.Hostname())
281+
if uHostname == "" || localhostRegex.MatchString(uHostname) {
282+
return errors.New("Bad url host")
283+
}
284+
285+
// if not allowed, reject credentialed/userinfo urls
286+
if !p.config.AllowCredetialURLs && reqURL.User != nil {
287+
return errors.New("Userinfo URL rejected")
288+
}
289+
290+
// ip/whitelist/blacklist filtering
291+
if !p.config.noIPFiltering {
292+
// if allowList is set, require match
293+
for _, rgx := range p.allowList {
294+
if rgx.MatchString(uHostname) {
295+
return errors.New("Allowlist host failure")
296+
}
297+
}
298+
299+
// filter out rejected networks
300+
if ip := net.ParseIP(uHostname); ip != nil {
301+
if isRejectedIP(ip) {
302+
return errors.New("Denylist host failure")
303+
}
304+
} else {
305+
if ips, err := net.LookupIP(uHostname); err == nil {
306+
for _, ip := range ips {
307+
if isRejectedIP(ip) {
308+
return errors.New("Denylist host failure")
309+
}
310+
}
311+
}
312+
}
313+
}
314+
315+
return nil
316+
}
317+
299318
// copy headers from src into dst
300319
// empty filter map will result in no filtering being done
301320
func (p *Proxy) copyHeaders(dst, src *http.Header, filter *map[string]bool) {
@@ -353,12 +372,6 @@ func New(pc Config) (*Proxy, error) {
353372
// timeout
354373
Timeout: pc.RequestTimeout,
355374
}
356-
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
357-
if len(via) >= pc.MaxRedirects {
358-
return errors.New("Too many redirects")
359-
}
360-
return nil
361-
}
362375

363376
var allow []*regexp.Regexp
364377
var c *regexp.Regexp
@@ -387,11 +400,28 @@ func New(pc Config) (*Proxy, error) {
387400
acceptTypesRe = append(acceptTypesRe, c)
388401
}
389402

390-
return &Proxy{
403+
p := &Proxy{
391404
client: client,
392405
config: &pc,
393406
allowList: allow,
394407
acceptTypesString: strings.Join(acceptTypes, ", "),
395-
acceptTypesRe: acceptTypesRe}, nil
408+
acceptTypesRe: acceptTypesRe,
409+
}
410+
411+
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
412+
if len(via) >= pc.MaxRedirects {
413+
mlog.Debug("Got bad redirect: Too many redirects")
414+
return errors.New("BadRedirect: Too many redirects")
415+
}
416+
err := p.checkURL(req.URL)
417+
if err != nil {
418+
mlog.Debugm("Got bad redirect", mlog.Map{"url": req})
419+
return fmt.Errorf("BadRedirect: %s", err)
420+
}
421+
422+
return nil
423+
}
424+
425+
return p, nil
396426

397427
}

pkg/camo/proxy_test.go

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,29 @@
55
package camo
66

77
import (
8+
"flag"
89
"fmt"
910
"net/http"
1011
"net/http/httptest"
12+
"os"
1113
"testing"
1214
"time"
1315

1416
"github.com/cactus/go-camo/pkg/camo/encoding"
1517
"github.com/cactus/go-camo/pkg/router"
18+
"github.com/cactus/mlog"
1619

1720
"github.com/stretchr/testify/assert"
1821
)
1922

2023
var camoConfig = Config{
21-
HMACKey: []byte("0x24FEEDFACEDEADBEEFCAFE"),
22-
MaxSize: 5120 * 1024,
23-
RequestTimeout: time.Duration(10) * time.Second,
24-
MaxRedirects: 3,
25-
ServerName: "go-camo",
26-
AllowContentVideo: false,
24+
HMACKey: []byte("0x24FEEDFACEDEADBEEFCAFE"),
25+
MaxSize: 5120 * 1024,
26+
RequestTimeout: time.Duration(10) * time.Second,
27+
MaxRedirects: 3,
28+
ServerName: "go-camo",
29+
AllowContentVideo: false,
30+
AllowCredetialURLs: false,
2731
}
2832

2933
func makeReq(testURL string) (*http.Request, error) {
@@ -196,6 +200,37 @@ func TestVideoContentTypeAllowed(t *testing.T) {
196200
assert.Nil(t, err)
197201
}
198202

203+
func TestCredetialURLsAllowed(t *testing.T) {
204+
t.Parallel()
205+
206+
camoConfigWithCredentials := Config{
207+
HMACKey: []byte("0x24FEEDFACEDEADBEEFCAFE"),
208+
MaxSize: 180 * 1024,
209+
RequestTimeout: time.Duration(10) * time.Second,
210+
MaxRedirects: 3,
211+
ServerName: "go-camo",
212+
AllowCredetialURLs: true,
213+
}
214+
215+
testURL := "http://user:[email protected]/images/srpr/logo11w.png"
216+
_, err := makeTestReq(testURL, 200, camoConfigWithCredentials)
217+
assert.Nil(t, err)
218+
}
219+
220+
func Test404OnVideo(t *testing.T) {
221+
t.Parallel()
222+
testURL := "http://mirrors.standaloneinstaller.com/video-sample/small.mp4"
223+
_, err := makeTestReq(testURL, 400, camoConfig)
224+
assert.Nil(t, err)
225+
}
226+
227+
func Test404OnCredentialURL(t *testing.T) {
228+
t.Parallel()
229+
testURL := "http://user:[email protected]/images/srpr/logo11w.png"
230+
_, err := makeTestReq(testURL, 404, camoConfig)
231+
assert.Nil(t, err)
232+
}
233+
199234
func Test404InfiniRedirect(t *testing.T) {
200235
t.Parallel()
201236
testURL := "http://httpbin.org/redirect/4"
@@ -286,6 +321,33 @@ func Test404OnLocalhostWithPort(t *testing.T) {
286321
}
287322
}
288323

324+
func Test404OnRedirectWithLocalhostTarget(t *testing.T) {
325+
t.Parallel()
326+
testURL := "http://httpbin.org/redirect-to?url=http://localhost/some.png"
327+
record, err := makeTestReq(testURL, 404, camoConfig)
328+
if assert.Nil(t, err) {
329+
assert.Equal(t, "Error Fetching Resource\n", record.Body.String(), "Expected 404 response body but got '%s' instead", record.Body.String())
330+
}
331+
}
332+
333+
func Test404OnRedirectWithLoopbackIP(t *testing.T) {
334+
t.Parallel()
335+
testURL := "http://httpbin.org/redirect-to?url=http://127.0.0.100/some.png"
336+
record, err := makeTestReq(testURL, 404, camoConfig)
337+
if assert.Nil(t, err) {
338+
assert.Equal(t, "Error Fetching Resource\n", record.Body.String(), "Expected 404 response body but got '%s' instead", record.Body.String())
339+
}
340+
}
341+
342+
func Test404OnRedirectWithLoopbackIPwCreds(t *testing.T) {
343+
t.Parallel()
344+
testURL := "http://httpbin.org/redirect-to?url=http://user:[email protected]/some.png"
345+
record, err := makeTestReq(testURL, 404, camoConfig)
346+
if assert.Nil(t, err) {
347+
assert.Equal(t, "Error Fetching Resource\n", record.Body.String(), "Expected 404 response body but got '%s' instead", record.Body.String())
348+
}
349+
}
350+
289351
// Test will fail if dns relay implements dns rebind prevention
290352
func Test404OnLoopback(t *testing.T) {
291353
t.Skip("Skipping test. CI environments generally enable something similar to unbound's private-address functionality, making this test fail.")
@@ -363,3 +425,18 @@ func TestTimeout(t *testing.T) {
363425

364426
close(cc)
365427
}
428+
429+
func TestMain(m *testing.M) {
430+
flag.Parse()
431+
432+
debug := os.Getenv("DEBUG")
433+
// now configure a standard logger
434+
mlog.SetFlags(mlog.Lstd)
435+
436+
if debug != "" {
437+
mlog.SetFlags(mlog.Flags() | mlog.Ldebug)
438+
mlog.Debug("debug logging enabled")
439+
}
440+
441+
os.Exit(m.Run())
442+
}

pkg/router/httpdate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ type iHTTPDate struct {
2323
func (h *iHTTPDate) String() string {
2424
stamp := h.dateValue.Load()
2525
if stamp == nil {
26-
mlog.Print("got a nil datesamp. Trying to recover...")
26+
mlog.Print("Got a nil datestamp. Trying to recover...")
2727
h.Update()
2828
return time.Now().UTC().Format(timeFormat)
2929
}

pkg/router/router.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,6 @@ func (dr *DumbRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
4040

4141
if r.Method != "HEAD" && r.Method != "GET" {
4242
http.Error(w, "Method Not Allowed", 405)
43-
}
44-
45-
components := strings.Split(r.URL.Path, "/")
46-
if len(components) == 3 {
47-
dr.CamoHandler.ServeHTTP(w, r)
4843
return
4944
}
5045

@@ -58,5 +53,11 @@ func (dr *DumbRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
5853
return
5954
}
6055

56+
components := strings.Split(r.URL.Path, "/")
57+
if len(components) == 3 {
58+
dr.CamoHandler.ServeHTTP(w, r)
59+
return
60+
}
61+
6162
http.Error(w, "404 Not Found", 404)
6263
}

0 commit comments

Comments
 (0)