@cryptotaxi247 / kubo / commits / 09937f84b

gateway: enforce allowlist for path prefixes

The gateway accepts an X-Ipfs-Path-Prefix header, and assumes that it is mounted in a reverse proxy like nginx, at this path. Links in directory listings, as well as trailing-slash redirects need to be rewritten with that prefix in mind. We don't want a potential attacker to be able to pass in arbitrary path prefixes, which would end up in redirects and directory listings, which is why every prefix has to be explicitly allowed in the config. Previously, we'd accept *any* X-Ipfs-Path-Prefix header. Example: We mount blog.ipfs.io (a dnslink page) at ipfs.io/blog. nginx_ipfs.conf: location /blog/ { rewrite "^/blog(/.*)$" $1 break; proxy_set_header Host blog.ipfs.io; proxy_set_header X-Ipfs-Gateway-Prefix /blog; proxy_pass http://127.0.0.1:8080; } .ipfs/config: "Gateway": { "PathPrefixes": ["/blog"], // ... }, dnslink: > dig TXT _dnslink.blog.ipfs.io dnslink=/ipfs/QmWcBjXPAEdhXDATV4ghUpkAonNBbiyFx1VmmHcQe9HEGd License: MIT Signed-off-by: Lars Gierth <larsg@systemli.org>

Lars Gierth committed Mar 28, 2016 at 19:21 UTC 09937f84b64212896e1d80147f828d86ebc19d68
8 files changed +72 -19
cmd/ipfs/daemon.go
+1 -1
@@ -448,7 +448,7 @@ func serveHTTPGateway(req cmds.Request) (error, <-chan error) {
448 corehttp.CommandsROOption(*req.InvocContext()),
449 corehttp.VersionOption(),
450 corehttp.IPNSHostnameOption(),
451 - corehttp.GatewayOption(writable),
451 + corehttp.GatewayOption(writable, cfg.Gateway.PathPrefixes),
452 }
453
454 if len(cfg.Gateway.RootRedirect) > 0 {
cmd/ipfswatch/main.go
+1 -1
@@ -84,7 +84,7 @@ func run(ipfsPath, watchPath string) error {
84 if *http {
85 addr := "/ip4/127.0.0.1/tcp/5001"
86 var opts = []corehttp.ServeOption{
87 - corehttp.GatewayOption(true),
87 + corehttp.GatewayOption(true, nil),
88 corehttp.WebUIOption,
89 corehttp.CommandsOption(cmdCtx(node, ipfsPath)),
90 }
core/corehttp/gateway.go
+8 -6
@@ -17,9 +17,10 @@ type Gateway struct {
17 }
18
19 type GatewayConfig struct {
20 - Headers map[string][]string
21 - BlockList *BlockList
22 - Writable bool
20 + Headers map[string][]string
21 + BlockList *BlockList
22 + Writable bool
23 + PathPrefixes []string
24 }
25
26 func NewGateway(conf GatewayConfig) *Gateway {
@@ -48,10 +49,11 @@ func (g *Gateway) ServeOption() ServeOption {
49 }
50 }
51
51 -func GatewayOption(writable bool) ServeOption {
52 +func GatewayOption(writable bool, prefixes []string) ServeOption {
53 g := NewGateway(GatewayConfig{
53 - Writable: writable,
54 - BlockList: &BlockList{},
54 + Writable: writable,
55 + BlockList: &BlockList{},
56 + PathPrefixes: prefixes,
57 })
58 return g.ServeOption()
59 }
core/corehttp/gateway_handler.go
+7 -2
@@ -131,8 +131,13 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
131 // It will be prepended to links in directory listings and the index.html redirect.
132 prefix := ""
133 if prefixHdr := r.Header["X-Ipfs-Gateway-Prefix"]; len(prefixHdr) > 0 {
134 - log.Debugf("X-Ipfs-Gateway-Prefix: %s", prefixHdr[0])
135 - prefix = prefixHdr[0]
134 + prfx := prefixHdr[0]
135 + for _, p := range i.config.PathPrefixes {
136 + if prfx == p || strings.HasPrefix(prfx, p+"/") {
137 + prefix = prfx
138 + break
139 + }
140 + }
141 }
142
143 // IPNSHostnameOption might have constructed an IPNS path using the Host header.
core/corehttp/gateway_test.go
+52 -8
@@ -98,7 +98,7 @@ func newTestServerAndNode(t *testing.T, ns mockNamesys) (*httptest.Server, *core
98 ts.Listener,
99 VersionOption(),
100 IPNSHostnameOption(),
101 - GatewayOption(false),
101 + GatewayOption(false, []string{"/good-prefix"}),
102 )
103 if err != nil {
104 t.Fatal(err)
@@ -227,7 +227,7 @@ func TestIPNSHostnameRedirect(t *testing.T) {
227 t.Fatal(err)
228 }
229 req.Host = "example.net"
230 - req.Header.Set("X-Ipfs-Gateway-Prefix", "/prefix")
230 + req.Header.Set("X-Ipfs-Gateway-Prefix", "/good-prefix")
231
232 res, err = doWithoutRedirect(req)
233 if err != nil {
@@ -241,8 +241,8 @@ func TestIPNSHostnameRedirect(t *testing.T) {
241 hdr = res.Header["Location"]
242 if len(hdr) < 1 {
243 t.Errorf("location header not present")
244 - } else if hdr[0] != "/prefix/foo/" {
245 - t.Errorf("location header is %v, expected /prefix/foo/", hdr[0])
244 + } else if hdr[0] != "/good-prefix/foo/" {
245 + t.Errorf("location header is %v, expected /good-prefix/foo/", hdr[0])
246 }
247 }
248
@@ -387,7 +387,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
387 t.Fatal(err)
388 }
389 req.Host = "example.net"
390 - req.Header.Set("X-Ipfs-Gateway-Prefix", "/prefix")
390 + req.Header.Set("X-Ipfs-Gateway-Prefix", "/good-prefix")
391
392 res, err = doWithoutRedirect(req)
393 if err != nil {
@@ -402,13 +402,57 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
402 s = string(body)
403 t.Logf("body: %s\n", string(body))
404
405 - if !strings.Contains(s, "Index of /prefix") {
405 + if !strings.Contains(s, "Index of /good-prefix") {
406 t.Fatalf("expected a path in directory listing")
407 }
408 - if !strings.Contains(s, "<a href=\"/prefix/\">") {
408 + if !strings.Contains(s, "<a href=\"/good-prefix/\">") {
409 t.Fatalf("expected backlink in directory listing")
410 }
411 - if !strings.Contains(s, "<a href=\"/prefix/file.txt\">") {
411 + if !strings.Contains(s, "<a href=\"/good-prefix/file.txt\">") {
412 + t.Fatalf("expected file in directory listing")
413 + }
414 +
415 + // make request to directory listing with illegal prefix
416 + req, err = http.NewRequest("GET", ts.URL, nil)
417 + if err != nil {
418 + t.Fatal(err)
419 + }
420 + req.Host = "example.net"
421 + req.Header.Set("X-Ipfs-Gateway-Prefix", "/bad-prefix")
422 +
423 + res, err = doWithoutRedirect(req)
424 + if err != nil {
425 + t.Fatal(err)
426 + }
427 +
428 + // make request to directory listing with evil prefix
429 + req, err = http.NewRequest("GET", ts.URL, nil)
430 + if err != nil {
431 + t.Fatal(err)
432 + }
433 + req.Host = "example.net"
434 + req.Header.Set("X-Ipfs-Gateway-Prefix", "//good-prefix/foo")
435 +
436 + res, err = doWithoutRedirect(req)
437 + if err != nil {
438 + t.Fatal(err)
439 + }
440 +
441 + // expect correct backlinks without illegal prefix
442 + body, err = ioutil.ReadAll(res.Body)
443 + if err != nil {
444 + t.Fatalf("error reading response: %s", err)
445 + }
446 + s = string(body)
447 + t.Logf("body: %s\n", string(body))
448 +
449 + if !strings.Contains(s, "Index of /") {
450 + t.Fatalf("expected a path in directory listing")
451 + }
452 + if !strings.Contains(s, "<a href=\"/\">") {
453 + t.Fatalf("expected backlink in directory listing")
454 + }
455 + if !strings.Contains(s, "<a href=\"/file.txt\">") {
456 t.Fatalf("expected file in directory listing")
457 }
458 }
repo/config/gateway.go
+1
@@ -5,4 +5,5 @@ type Gateway struct {
5 HTTPHeaders map[string][]string // HTTP headers to return with the gateway
6 RootRedirect string
7 Writable bool
8 + PathPrefixes []string
9 }
repo/config/init.go
+1
@@ -65,6 +65,7 @@ func Init(out io.Writer, nBitsForKeypair int) (*Config, error) {
65 Gateway: Gateway{
66 RootRedirect: "",
67 Writable: false,
68 + PathPrefixes: []string{},
69 },
70 }
71
test/supernode_client/main.go
+1 -1
@@ -109,7 +109,7 @@ func run() error {
109
110 opts := []corehttp.ServeOption{
111 corehttp.CommandsOption(cmdCtx(node, repoPath)),
112 - corehttp.GatewayOption(false),
112 + corehttp.GatewayOption(false, nil),
113 }
114
115 if *cat {