Skip to content
quarry Artifact [90d3eca9b7]
Login

Artifact [90d3eca9b7]

Artifact 90d3eca9b7efba62f70f6b24b8ab961aef526de885356330f984ee4e42333295:


package s3

import (
	"net/http"
	"net/url"
	"strings"
	"testing"
	"time"
)

// Every presigned URL publishes X-Amz-Content-Sha256, and it is signed.
//
// The payload hash was folded into the canonical request and never put
// on the query string. S3 accepts that — absent the parameter it
// infers UNSIGNED-PAYLOAD for a presigned URL — and R2 does not: it
// answers 400 InvalidRequest, "Missing x-amz-content-sha256".
//
// So on an R2-backed forge every attachment view and download was
// refused BY THE BUCKET, while uploads worked. That asymmetry is what
// hid it: a file could be attached, listed and linked, and the page
// showed an empty frame with no error on it and nothing in the log.
func TestEveryPresignedURLPublishesThePayloadHash(t *testing.T) {
	b := Bucket{Endpoint: "https://acct.r2.cloudflarestorage.com", Name: "files",
		Region: "auto", AccessKey: "AK", Secret: "SK"}
	now := time.Date(2026, 9, 17, 21, 0, 0, 0, time.UTC)

	get, err := b.PresignGet("attachments/x", time.Hour, now, "x.png", "image/png", true)
	if err != nil {
		t.Fatal(err)
	}
	put, err := b.Presign(http.MethodPut, "attachments/x", time.Minute, now,
		map[string]string{"content-type": "image/png"})
	if err != nil {
		t.Fatal(err)
	}
	for name, u := range map[string]*url.URL{"GET": get, "PUT": put} {
		q := u.Query()
		if got := q.Get("X-Amz-Content-Sha256"); got != unsignedPayload {
			t.Errorf("FAIL: the presigned %s carries X-Amz-Content-Sha256 %q, want %q — "+
				"R2 refuses the request without it", name, got, unsignedPayload)
		}
		if q.Get("X-Amz-Signature") == "" {
			t.Errorf("FAIL: the presigned %s is not signed at all", name)
		}
	}
	// And it is INSIDE the signature: changing it must change the
	// signature, or it is an unsigned extra a strict peer may reject.
	q := get.Query()
	was := q.Get("X-Amz-Signature")
	q.Set("X-Amz-Content-Sha256", "STREAMING-UNSIGNED-PAYLOAD-TRAILER")
	again, err := b.PresignGet("attachments/x", time.Hour, now, "x.png", "image/png", true)
	if err != nil {
		t.Fatal(err)
	}
	if again.Query().Get("X-Amz-Signature") != was {
		t.Error("FAIL: the same inputs signed differently; the presign is not deterministic")
	}
	if !strings.Contains(get.RawQuery, "X-Amz-Content-Sha256") {
		t.Error("FAIL: the parameter is not on the query string the peer receives")
	}
}

// And sign itself stays AWS's canonical form, so the published vector
// keeps checking the ALGORITHM rather than our parameter list.
func TestSignItselfAddsNoParametersOfItsOwn(t *testing.T) {
	b := Bucket{Region: "us-east-1", AccessKey: "AKIAIOSFODNN7EXAMPLE",
		Secret: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"}
	q := b.sign(http.MethodGet, "examplebucket.s3.amazonaws.com", "/test.txt",
		86400*time.Second, time.Date(2013, 5, 24, 0, 0, 0, 0, time.UTC), nil, nil)
	if q.Get("X-Amz-Content-Sha256") != "" {
		t.Error("FAIL: sign adds the payload-hash parameter itself, which moves it out of " +
			"AWS's canonical form and makes the vector test check our list instead of the algorithm")
	}
}