Validate dimensions and fit mode on encrypted URLs (closes #62)
check / check (push) Successful in 3m6s
check / check (push) Successful in 3m6s
The encrypted /v1/e/ route used the decrypted payload unchecked, so a token could request an over-limit size or an unknown fit mode; the generator turned unparseable numbers into 0. imgcache.ValidateDimension alone holds the MaxDimension bound and is used by the path parser, by the new ValidateImageRequest (which adds ValidateFitMode) and by the generator. Both image routes call ValidateImageRequest, so each answers 400. The generator answers 400 naming the field for a width or height that is not a number or fails that check, a quality that is not a number from 1 to 100, a ttl that is not a number from 0 to the largest the expiry calculation can hold, or an unknown fit. Empty quality is 85; empty ttl never expires. The form's size inputs stop at 8192. Model: opus-4-8 (implementation); opus-5-5 (rework)
This commit was merged in pull request #126.
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/pixa/internal/encurl"
|
||||
"sneak.berlin/go/pixa/internal/imgcache"
|
||||
)
|
||||
|
||||
// Generator form field names, and a value that is not a number.
|
||||
const (
|
||||
widthField = "width"
|
||||
heightField = "height"
|
||||
qualityField = "quality"
|
||||
ttlField = "ttl"
|
||||
fitField = "fit"
|
||||
notANumber = "abc"
|
||||
)
|
||||
|
||||
// generatePost submits the /generate form with a valid session and CSRF token
|
||||
// plus the caller's extra fields, returning the recorder.
|
||||
func generatePost(
|
||||
t *testing.T, extra url.Values,
|
||||
) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
|
||||
h, srv := newCSRFTestRouter(t)
|
||||
|
||||
sessionCookie := newSessionCookie(t, h)
|
||||
cookies, token := csrfCredentials(t, srv, []*http.Cookie{sessionCookie})
|
||||
cookies = append(cookies, sessionCookie)
|
||||
|
||||
form := url.Values{
|
||||
sourceURLField: {testSourceURL},
|
||||
csrfTokenField: {token},
|
||||
}
|
||||
maps.Copy(form, extra)
|
||||
|
||||
return postForm(srv, "/generate", cookies, form)
|
||||
}
|
||||
|
||||
// TestGeneratePostRejectsNonNumericWidth verifies that a non-numeric width is
|
||||
// rejected with 400 naming the field rather than being coerced to 0 and
|
||||
// minting a 0-width token.
|
||||
func TestGeneratePostRejectsNonNumericWidth(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec := generatePost(t, url.Values{"width": {"abc"}})
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if strings.Contains(rec.Body.String(), "/v1/e/") {
|
||||
t.Error("a token was generated for non-numeric width")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGeneratePostRejectsOverLimitWidth verifies that a width beyond
|
||||
// MaxDimension is rejected at generation time so an unusable token cannot be
|
||||
// minted.
|
||||
func TestGeneratePostRejectsOverLimitWidth(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec := generatePost(t, url.Values{"width": {"100000"}})
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if strings.Contains(rec.Body.String(), "/v1/e/") {
|
||||
t.Error("a token was generated for an over-limit width")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGeneratePostRejectsBadField verifies that each generator field whose
|
||||
// value is not a number, out of range, or unrecognized is rejected with 400,
|
||||
// mints no token, and is named in the error shown on the page.
|
||||
func TestGeneratePostRejectsBadField(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
field, value, wantError string
|
||||
}{
|
||||
{widthField, notANumber, "invalid width: not a number"},
|
||||
{widthField, "-1", "width is negative"},
|
||||
{widthField, "8193", "width is above 8192"},
|
||||
{heightField, notANumber, "invalid height: not a number"},
|
||||
{heightField, "8193", "height is above 8192"},
|
||||
{qualityField, notANumber, "invalid quality: not a number"},
|
||||
{qualityField, "0", "invalid quality: must be from 1 to 100"},
|
||||
{qualityField, "101", "invalid quality: must be from 1 to 100"},
|
||||
{ttlField, notANumber, "invalid ttl: not a number"},
|
||||
{ttlField, "-1", "invalid ttl: must be from 0 to"},
|
||||
{ttlField, "10000000000", "invalid ttl: must be from 0 to"},
|
||||
{fitField, "bogus", "invalid fit mode: bogus"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.field+"="+tt.value, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec := generatePost(t, url.Values{tt.field: {tt.value}})
|
||||
body := rec.Body.String()
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if strings.Contains(body, "/v1/e/") {
|
||||
t.Error("a token was generated")
|
||||
}
|
||||
|
||||
if !strings.Contains(body, tt.wantError) {
|
||||
t.Errorf("page does not show %q", tt.wantError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildGeneratePayloadDefaultAndLimits verifies that an empty quality
|
||||
// takes the default, and that the largest accepted width, height, quality and
|
||||
// ttl are accepted with an expiry still in the future.
|
||||
func TestBuildGeneratePayloadDefaultAndLimits(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
parsed, err := url.Parse(testSourceURL)
|
||||
if err != nil {
|
||||
t.Fatalf("url.Parse() error = %v", err)
|
||||
}
|
||||
|
||||
payload, _, _, err := buildGeneratePayload(parsed, url.Values{})
|
||||
if err != nil {
|
||||
t.Fatalf("empty form: error = %v", err)
|
||||
}
|
||||
|
||||
if payload.Quality != encurl.DefaultQuality {
|
||||
t.Errorf("empty quality gave %d, want %d",
|
||||
payload.Quality, encurl.DefaultQuality)
|
||||
}
|
||||
|
||||
_, expiresAt, _, err := buildGeneratePayload(parsed, url.Values{
|
||||
widthField: {strconv.Itoa(imgcache.MaxDimension)},
|
||||
heightField: {strconv.Itoa(imgcache.MaxDimension)},
|
||||
qualityField: {strconv.Itoa(maxQuality)},
|
||||
ttlField: {strconv.Itoa(maxTTL)},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("largest accepted values: error = %v", err)
|
||||
}
|
||||
|
||||
if !expiresAt.After(time.Now()) {
|
||||
t.Errorf("ttl %d gave expiry %v, want a time in the future",
|
||||
maxTTL, expiresAt)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user