@cryptotaxi247 / kubo / commits / 5fa556945

fix(gw): validate requested CAR version (#8835)

* fix(gw): validate requested CAR version This adds validation of 'application/vnd.ipld.car;version=n' passed in the Accept header by HTTP clients to align Gateway behavior with the spec submitted to IANA. * test: fix comment in test/sharness/t0118-gateway-car.sh Co-authored-by: Gus Eggert <gus@gus.dev> Co-authored-by: Gus Eggert <gus@gus.dev>

Marcin Rataj committed Apr 1, 2022 at 18:12 UTC 5fa556945e2a9733f39e1bfcc242cba4c31c070b
3 files changed +46 -11
core/corehttp/gateway_handler.go
+20 -10
@@ -5,6 +5,7 @@ import (
5 "fmt"
6 "html/template"
7 "io"
8 + "mime"
9 "net/http"
10 "net/url"
11 "os"
@@ -348,7 +349,11 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
349 }
350
351 // Detect when explicit Accept header or ?format parameter are present
351 - responseFormat := customResponseFormat(r)
352 + responseFormat, formatParams, err := customResponseFormat(r)
353 + if err != nil {
354 + webError(w, "error while processing the Accept header", err, http.StatusBadRequest)
355 + return
356 + }
357
358 // Finish early if client already has matching Etag
359 if r.Header.Get("If-None-Match") == getEtag(r, resolvedPath.Cid()) {
@@ -389,9 +394,10 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
394 logger.Debugw("serving raw block", "path", contentPath)
395 i.serveRawBlock(w, r, resolvedPath.Cid(), contentPath, begin)
396 return
392 - case "application/vnd.ipld.car", "application/vnd.ipld.car; version=1":
397 + case "application/vnd.ipld.car":
398 logger.Debugw("serving car stream", "path", contentPath)
394 - i.serveCar(w, r, resolvedPath.Cid(), contentPath, begin)
399 + carVersion := formatParams["version"]
400 + i.serveCar(w, r, resolvedPath.Cid(), contentPath, carVersion, begin)
401 return
402 default: // catch-all for unsuported application/vnd.*
403 err := fmt.Errorf("unsupported format %q", responseFormat)
@@ -761,8 +767,8 @@ func getFilename(contentPath ipath.Path) string {
767 func getEtag(r *http.Request, cid cid.Cid) string {
768 prefix := `"`
769 suffix := `"`
764 - responseFormat := customResponseFormat(r)
765 - if responseFormat != "" {
770 + responseFormat, _, err := customResponseFormat(r)
771 + if err == nil && responseFormat != "" {
772 // application/vnd.ipld.foo → foo
773 f := responseFormat[strings.LastIndex(responseFormat, ".")+1:]
774 // Etag: "cid.foo" (gives us nice compression together with Content-Disposition in block (raw) and car responses)
@@ -773,14 +779,14 @@ func getEtag(r *http.Request, cid cid.Cid) string {
779 }
780
781 // return explicit response format if specified in request as query parameter or via Accept HTTP header
776 -func customResponseFormat(r *http.Request) string {
782 +func customResponseFormat(r *http.Request) (mediaType string, params map[string]string, err error) {
783 if formatParam := r.URL.Query().Get("format"); formatParam != "" {
784 // translate query param to a content type
785 switch formatParam {
786 case "raw":
781 - return "application/vnd.ipld.raw"
787 + return "application/vnd.ipld.raw", nil, nil
788 case "car":
783 - return "application/vnd.ipld.car"
789 + return "application/vnd.ipld.car", nil, nil
790 }
791 }
792 // Browsers and other user agents will send Accept header with generic types like:
@@ -789,10 +795,14 @@ func customResponseFormat(r *http.Request) string {
795 for _, accept := range r.Header.Values("Accept") {
796 // respond to the very first ipld content type
797 if strings.HasPrefix(accept, "application/vnd.ipld") {
792 - return accept
798 + mediatype, params, err := mime.ParseMediaType(accept)
799 + if err != nil {
800 + return "", nil, err
801 + }
802 + return mediatype, params, nil
803 }
804 }
795 - return ""
805 + return "", nil, nil
806 }
807
808 func (i *gatewayHandler) searchUpTreeFor404(r *http.Request, contentPath ipath.Path) (ipath.Resolved, string, error) {
core/corehttp/gateway_handler_car.go
+11 -1
@@ -2,6 +2,7 @@ package corehttp
2
3 import (
4 "context"
5 + "fmt"
6 "net/http"
7 "time"
8
@@ -14,10 +15,19 @@ import (
15 )
16
17 // serveCar returns a CAR stream for specific DAG+selector
17 -func (i *gatewayHandler) serveCar(w http.ResponseWriter, r *http.Request, rootCid cid.Cid, contentPath ipath.Path, begin time.Time) {
18 +func (i *gatewayHandler) serveCar(w http.ResponseWriter, r *http.Request, rootCid cid.Cid, contentPath ipath.Path, carVersion string, begin time.Time) {
19 ctx, cancel := context.WithCancel(r.Context())
20 defer cancel()
21
22 + switch carVersion {
23 + case "": // noop, client does not care about version
24 + case "1": // noop, we support this
25 + default:
26 + err := fmt.Errorf("only version=1 is supported")
27 + webError(w, "unsupported CAR version", err, http.StatusBadRequest)
28 + return
29 + }
30 +
31 // Set Content-Disposition
32 name := rootCid.String() + ".car"
33 setContentDispositionHeader(w, name, "attachment")
test/sharness/t0118-gateway-car.sh
+15
@@ -51,10 +51,25 @@ test_launch_ipfs_daemon_without_network
51 # explicit version=1
52 test_expect_success "GET for application/vnd.ipld.raw version=1 returns a CARv1 stream" '
53 ipfs dag import test-dag.car &&
54 + curl -sX GET -H "Accept: application/vnd.ipld.car;version=1" "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT_DIR_CID/subdir/ascii.txt" -o gateway-header-v1.car &&
55 + test_cmp deterministic.car gateway-header-v1.car
56 + '
57 +
58 + # explicit version=1 with whitepace
59 + test_expect_success "GET for application/vnd.ipld.raw version=1 returns a CARv1 stream (with whitespace)" '
60 + ipfs dag import test-dag.car &&
61 curl -sX GET -H "Accept: application/vnd.ipld.car; version=1" "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT_DIR_CID/subdir/ascii.txt" -o gateway-header-v1.car &&
62 test_cmp deterministic.car gateway-header-v1.car
63 '
64
65 + # explicit version=2
66 + test_expect_success "GET for application/vnd.ipld.raw version=2 returns HTTP 400 Bad Request error" '
67 + curl -svX GET -H "Accept: application/vnd.ipld.car;version=2" "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT_DIR_CID/subdir/ascii.txt" > curl_output 2>&1 &&
68 + cat curl_output &&
69 + grep "400 Bad Request" curl_output &&
70 + grep "unsupported CAR version" curl_output
71 + '
72 +
73 # GET unixfs directory as a CAR with DAG and some selector
74
75 # TODO: this is basic test for "full" selector, we will add support for custom ones in https://github.com/ipfs/go-ipfs/issues/8769