diff --git a/internal/signature/quality_fit_test.go b/internal/signature/quality_fit_test.go new file mode 100644 index 0000000..0c82c5a --- /dev/null +++ b/internal/signature/quality_fit_test.go @@ -0,0 +1,68 @@ +package signature_test + +import ( + "errors" + "testing" + "time" + + "sneak.berlin/go/pixa/internal/signature" +) + +// signedQualityFitRequest returns a request signed for quality 85 and fit +// mode "cover", the effective defaults the handler applies before +// verification. +func signedQualityFitRequest(signer *signature.Signer) *signature.Request { + req := &signature.Request{ + SourceHost: testHost, + SourcePath: testPath, + Width: 800, + Height: 600, + Format: testFormatWebP, + Quality: 85, + FitMode: "cover", + Expires: time.Now().Add(1 * time.Hour), + } + req.Signature = signer.Sign(req) + + return req +} + +// TestSigner_Verify_QualityAndFitAreSigned proves that quality and fit are +// covered by the signature: a URL signed for one quality or fit mode must +// not verify when replayed with a different quality or fit mode. This is the +// amplification vector from the issue — one signed URL replayed across many +// quality and fit values yields many unauthorized cache entries and +// transcodes — so it must be rejected. +func TestSigner_Verify_QualityAndFitAreSigned(t *testing.T) { + t.Parallel() + + signer := signature.New("test-secret-key") + + cases := []struct { + name string + tamper func(r *signature.Request) + }{ + { + name: "replayed with different quality", + tamper: func(r *signature.Request) { r.Quality = 40 }, + }, + { + name: "replayed with different fit mode", + tamper: func(r *signature.Request) { r.FitMode = "contain" }, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + req := signedQualityFitRequest(signer) + tt.tamper(req) + + err := signer.Verify(req) + if !errors.Is(err, signature.ErrInvalid) { + t.Errorf("Verify() = %v, want %v", err, signature.ErrInvalid) + } + }) + } +} diff --git a/internal/signature/signature.go b/internal/signature/signature.go index 1a5b00e..f044ffa 100644 --- a/internal/signature/signature.go +++ b/internal/signature/signature.go @@ -37,6 +37,15 @@ type Request struct { Height int // Format is the requested output format (e.g. "webp"). Format string + // Quality is the requested output quality (1-100) for lossy formats. + // It is the effective value the request resolves to: callers pass the + // default quality when the request omits the parameter, so an omitted + // quality signs identically to that same value stated explicitly. + Quality int + // FitMode is how the image is fit into the requested dimensions + // (e.g. "cover"). Like Quality it is the effective value: callers pass + // the default fit mode when the request omits the parameter. + FitMode string // Signature is the HMAC signature to verify. Signature string // Expires is the signature expiration timestamp.