Files
pixa/internal/signature/quality_fit_test.go
T
sneak 8a03d0dbe7
check / check (push) Canceled after 0s
Take the signature, the expiry and the URL scheme off the image request (closes #314)
The image request now holds only its source and its transform, exactly what
the variant key covers. The /v1/image/ handler checks the allowlist and
verifies the signature itself with Signer.Verify(req, sig, expires). Both
image handlers keep the URL's expiry as a local value for Cache-Control. The
image service holds no signer, allowlist or AllowHTTP, and ValidateRequest is
gone. The fetcher takes the source and builds its URL with Source.URL from its
own AllowHTTP, so no URL string is built and parsed again. chooseAutoFormat
returns the format it chooses instead of writing into the request. The signed
URL format and every route's answers are unchanged.

Model: opus-5-5
2026-10-26 12:18:23 +00:00

74 lines
2.0 KiB
Go

package signature_test
import (
"errors"
"testing"
"time"
"sneak.berlin/go/pixa/internal/imagerequest"
"sneak.berlin/go/pixa/internal/signature"
)
// signedQualityFitRequest returns a request, its signature for quality 85 and
// fit mode "cover", the effective defaults the handler applies before
// verification, and the expiry it was signed with.
func signedQualityFitRequest(
signer *signature.Signer,
) (*imagerequest.Request, string, time.Time) {
req := &imagerequest.Request{
Source: imagerequest.Source{
Host: testHost,
Path: testPath,
},
Transform: imagerequest.Transform{
Size: imagerequest.Size{Width: 800, Height: 600},
Format: testFormatWebP,
Quality: 85,
FitMode: testFitCover,
},
}
expires := time.Now().Add(1 * time.Hour)
return req, signer.Sign(req, expires), expires
}
// 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 *imagerequest.Request)
}{
{
name: "replayed with different quality",
tamper: func(r *imagerequest.Request) { r.Transform.Quality = 40 },
},
{
name: "replayed with different fit mode",
tamper: func(r *imagerequest.Request) { r.Transform.FitMode = testFitContain },
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
req, sig, expires := signedQualityFitRequest(signer)
tt.tamper(req)
err := signer.Verify(req, sig, expires)
if !errors.Is(err, signature.ErrInvalid) {
t.Errorf("Verify() = %v, want %v", err, signature.ErrInvalid)
}
})
}
}