提交 09937f84 作者: Lars Gierth

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: 's avatarLars Gierth <larsg@systemli.org>
上级 8acd87d7
...@@ -448,7 +448,7 @@ func serveHTTPGateway(req cmds.Request) (error, <-chan error) { ...@@ -448,7 +448,7 @@ func serveHTTPGateway(req cmds.Request) (error, <-chan error) {
corehttp.CommandsROOption(*req.InvocContext()), corehttp.CommandsROOption(*req.InvocContext()),
corehttp.VersionOption(), corehttp.VersionOption(),
corehttp.IPNSHostnameOption(), corehttp.IPNSHostnameOption(),
corehttp.GatewayOption(writable), corehttp.GatewayOption(writable, cfg.Gateway.PathPrefixes),
} }
if len(cfg.Gateway.RootRedirect) > 0 { if len(cfg.Gateway.RootRedirect) > 0 {
......
...@@ -84,7 +84,7 @@ func run(ipfsPath, watchPath string) error { ...@@ -84,7 +84,7 @@ func run(ipfsPath, watchPath string) error {
if *http { if *http {
addr := "/ip4/127.0.0.1/tcp/5001" addr := "/ip4/127.0.0.1/tcp/5001"
var opts = []corehttp.ServeOption{ var opts = []corehttp.ServeOption{
corehttp.GatewayOption(true), corehttp.GatewayOption(true, nil),
corehttp.WebUIOption, corehttp.WebUIOption,
corehttp.CommandsOption(cmdCtx(node, ipfsPath)), corehttp.CommandsOption(cmdCtx(node, ipfsPath)),
} }
......
...@@ -17,9 +17,10 @@ type Gateway struct { ...@@ -17,9 +17,10 @@ type Gateway struct {
} }
type GatewayConfig struct { type GatewayConfig struct {
Headers map[string][]string Headers map[string][]string
BlockList *BlockList BlockList *BlockList
Writable bool Writable bool
PathPrefixes []string
} }
func NewGateway(conf GatewayConfig) *Gateway { func NewGateway(conf GatewayConfig) *Gateway {
...@@ -48,10 +49,11 @@ func (g *Gateway) ServeOption() ServeOption { ...@@ -48,10 +49,11 @@ func (g *Gateway) ServeOption() ServeOption {
} }
} }
func GatewayOption(writable bool) ServeOption { func GatewayOption(writable bool, prefixes []string) ServeOption {
g := NewGateway(GatewayConfig{ g := NewGateway(GatewayConfig{
Writable: writable, Writable: writable,
BlockList: &BlockList{}, BlockList: &BlockList{},
PathPrefixes: prefixes,
}) })
return g.ServeOption() return g.ServeOption()
} }
......
...@@ -131,8 +131,13 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request ...@@ -131,8 +131,13 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
// It will be prepended to links in directory listings and the index.html redirect. // It will be prepended to links in directory listings and the index.html redirect.
prefix := "" prefix := ""
if prefixHdr := r.Header["X-Ipfs-Gateway-Prefix"]; len(prefixHdr) > 0 { if prefixHdr := r.Header["X-Ipfs-Gateway-Prefix"]; len(prefixHdr) > 0 {
log.Debugf("X-Ipfs-Gateway-Prefix: %s", prefixHdr[0]) prfx := prefixHdr[0]
prefix = prefixHdr[0] for _, p := range i.config.PathPrefixes {
if prfx == p || strings.HasPrefix(prfx, p+"/") {
prefix = prfx
break
}
}
} }
// IPNSHostnameOption might have constructed an IPNS path using the Host header. // IPNSHostnameOption might have constructed an IPNS path using the Host header.
......
...@@ -98,7 +98,7 @@ func newTestServerAndNode(t *testing.T, ns mockNamesys) (*httptest.Server, *core ...@@ -98,7 +98,7 @@ func newTestServerAndNode(t *testing.T, ns mockNamesys) (*httptest.Server, *core
ts.Listener, ts.Listener,
VersionOption(), VersionOption(),
IPNSHostnameOption(), IPNSHostnameOption(),
GatewayOption(false), GatewayOption(false, []string{"/good-prefix"}),
) )
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
...@@ -227,7 +227,7 @@ func TestIPNSHostnameRedirect(t *testing.T) { ...@@ -227,7 +227,7 @@ func TestIPNSHostnameRedirect(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
req.Host = "example.net" req.Host = "example.net"
req.Header.Set("X-Ipfs-Gateway-Prefix", "/prefix") req.Header.Set("X-Ipfs-Gateway-Prefix", "/good-prefix")
res, err = doWithoutRedirect(req) res, err = doWithoutRedirect(req)
if err != nil { if err != nil {
...@@ -241,8 +241,8 @@ func TestIPNSHostnameRedirect(t *testing.T) { ...@@ -241,8 +241,8 @@ func TestIPNSHostnameRedirect(t *testing.T) {
hdr = res.Header["Location"] hdr = res.Header["Location"]
if len(hdr) < 1 { if len(hdr) < 1 {
t.Errorf("location header not present") t.Errorf("location header not present")
} else if hdr[0] != "/prefix/foo/" { } else if hdr[0] != "/good-prefix/foo/" {
t.Errorf("location header is %v, expected /prefix/foo/", hdr[0]) t.Errorf("location header is %v, expected /good-prefix/foo/", hdr[0])
} }
} }
...@@ -387,7 +387,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) { ...@@ -387,7 +387,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
req.Host = "example.net" req.Host = "example.net"
req.Header.Set("X-Ipfs-Gateway-Prefix", "/prefix") req.Header.Set("X-Ipfs-Gateway-Prefix", "/good-prefix")
res, err = doWithoutRedirect(req) res, err = doWithoutRedirect(req)
if err != nil { if err != nil {
...@@ -402,13 +402,57 @@ func TestIPNSHostnameBacklinks(t *testing.T) { ...@@ -402,13 +402,57 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
s = string(body) s = string(body)
t.Logf("body: %s\n", string(body)) t.Logf("body: %s\n", string(body))
if !strings.Contains(s, "Index of /prefix") { if !strings.Contains(s, "Index of /good-prefix") {
t.Fatalf("expected a path in directory listing") t.Fatalf("expected a path in directory listing")
} }
if !strings.Contains(s, "<a href=\"/prefix/\">") { if !strings.Contains(s, "<a href=\"/good-prefix/\">") {
t.Fatalf("expected backlink in directory listing") t.Fatalf("expected backlink in directory listing")
} }
if !strings.Contains(s, "<a href=\"/prefix/file.txt\">") { if !strings.Contains(s, "<a href=\"/good-prefix/file.txt\">") {
t.Fatalf("expected file in directory listing")
}
// make request to directory listing with illegal prefix
req, err = http.NewRequest("GET", ts.URL, nil)
if err != nil {
t.Fatal(err)
}
req.Host = "example.net"
req.Header.Set("X-Ipfs-Gateway-Prefix", "/bad-prefix")
res, err = doWithoutRedirect(req)
if err != nil {
t.Fatal(err)
}
// make request to directory listing with evil prefix
req, err = http.NewRequest("GET", ts.URL, nil)
if err != nil {
t.Fatal(err)
}
req.Host = "example.net"
req.Header.Set("X-Ipfs-Gateway-Prefix", "//good-prefix/foo")
res, err = doWithoutRedirect(req)
if err != nil {
t.Fatal(err)
}
// expect correct backlinks without illegal prefix
body, err = ioutil.ReadAll(res.Body)
if err != nil {
t.Fatalf("error reading response: %s", err)
}
s = string(body)
t.Logf("body: %s\n", string(body))
if !strings.Contains(s, "Index of /") {
t.Fatalf("expected a path in directory listing")
}
if !strings.Contains(s, "<a href=\"/\">") {
t.Fatalf("expected backlink in directory listing")
}
if !strings.Contains(s, "<a href=\"/file.txt\">") {
t.Fatalf("expected file in directory listing") t.Fatalf("expected file in directory listing")
} }
} }
......
...@@ -5,4 +5,5 @@ type Gateway struct { ...@@ -5,4 +5,5 @@ type Gateway struct {
HTTPHeaders map[string][]string // HTTP headers to return with the gateway HTTPHeaders map[string][]string // HTTP headers to return with the gateway
RootRedirect string RootRedirect string
Writable bool Writable bool
PathPrefixes []string
} }
...@@ -65,6 +65,7 @@ func Init(out io.Writer, nBitsForKeypair int) (*Config, error) { ...@@ -65,6 +65,7 @@ func Init(out io.Writer, nBitsForKeypair int) (*Config, error) {
Gateway: Gateway{ Gateway: Gateway{
RootRedirect: "", RootRedirect: "",
Writable: false, Writable: false,
PathPrefixes: []string{},
}, },
} }
......
...@@ -109,7 +109,7 @@ func run() error { ...@@ -109,7 +109,7 @@ func run() error {
opts := []corehttp.ServeOption{ opts := []corehttp.ServeOption{
corehttp.CommandsOption(cmdCtx(node, repoPath)), corehttp.CommandsOption(cmdCtx(node, repoPath)),
corehttp.GatewayOption(false), corehttp.GatewayOption(false, nil),
} }
if *cat { if *cat {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论