Skip to content

Commit 3b7d2b0

Browse files
committed
Refactor filesystem operations to validate repository paths and references
This commit introduces robust validation for repository paths and references within the filesystem operations. In internal/storage/filesystem.go, the sanitizeReference function is updated to return an error for invalid reference strings, including checks for empty strings, path separators, and invalid characters. This change is mirrored in the PutManifest function to ensure that references are sanitized before being used to construct paths. The Filesystem struct methods, specifically ManifestPath, PutManifest, and linkManifestTag, now explicitly check for errors returned by sanitizeReference, ensuring that only valid references are processed. Tests are updated to validate this new behavior. internal/storage/filesystem_test.go now includes a new test, TestFilesystemRejectsUnsafeTagReferences, which attempts to use various unsafe tag references to confirm that PutManifest, LinkManifestTag, and GetManifest reject them, enforcing the new validation logic. Additionally, internal/auth/password.go is updated to enforce a check against parallelism values in argon2 parameters, preventing overflows when setting secret hash parallelism. Configuration changes in internal/config/config.go update the structure of HTTPConfig and Default() to explicitly set SecureCookies to true, aligning with potential security requirements. Tests in
1 parent 9109b8f commit 3b7d2b0

13 files changed

Lines changed: 192 additions & 34 deletions

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ The published Docker image starts SCR with `-config /etc/scr/config.yaml`. The i
5252
http:
5353
address: "0.0.0.0"
5454
port: 5000
55+
secureCookies: true
5556

5657
storage:
5758
rootDirectory: "/var/lib/scr/registry"
@@ -84,6 +85,7 @@ docker run --rm --name scr \
8485
Configuration supports these sections:
8586

8687
- `http.address` and `http.port`
88+
- `http.secureCookies`; defaults to `true`. Leave enabled when SCR is accessed over HTTPS, including behind an HTTPS-terminating reverse proxy. Set to `false` only when serving the admin UI directly over plain HTTP.
8789
- `storage.rootDirectory`
8890
- `storage.gc`
8991
- `storage.gcDelay`
@@ -111,6 +113,14 @@ Bootstrap admin username and password are normally provided with environment var
111113

112114
If bootstrap admin values are omitted from the config file, SCR fills them from those environment variables. Provide both values together.
113115

116+
### Admin UI cookies and reverse proxies
117+
118+
SCR stores admin UI sessions in an `HttpOnly`, `SameSite=Lax` cookie. By default, `http.secureCookies` is `true`, which also marks that cookie `Secure` so browsers only send it over HTTPS.
119+
120+
Keep `http.secureCookies: true` for production deployments, including the common setup where a reverse proxy terminates HTTPS and forwards plain HTTP to SCR. The browser only sees the public HTTPS URL, so the `Secure` cookie works normally even if the proxy-to-SCR hop is HTTP.
121+
122+
Set `http.secureCookies: false` only when users access SCR directly over plain HTTP, such as a local development instance or a trusted internal HTTP-only deployment. Do not disable it for an HTTPS reverse-proxy deployment.
123+
114124
## Authentication and access
115125

116126
Registry clients use Docker-compatible bearer-token authentication:

config.container.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
http:
22
address: "0.0.0.0"
33
port: 5000
4+
secureCookies: true
45

56
storage:
67
rootDirectory: "/var/lib/scr/registry"

config.example.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
http:
22
address: "0.0.0.0"
33
port: 5000
4+
secureCookies: true
45

56
storage:
67
rootDirectory: "/var/lib/scr/registry"

config.local.example.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
http:
22
address: "0.0.0.0"
33
port: 5000
4+
# Set true when accessing SCR through HTTPS, including an HTTPS-terminating reverse proxy.
5+
secureCookies: false
46

57
storage:
68
rootDirectory: "data/registry"

internal/auth/password.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ func decodeHash(encoded string) (argonParams, []byte, []byte, error) {
8383
case "t":
8484
params.iterations = uint32(parsed)
8585
case "p":
86+
if parsed > 255 {
87+
return argonParams{}, nil, nil, errors.New("secret hash parallelism is too large")
88+
}
8689
params.parallelism = uint8(parsed)
8790
default:
8891
return argonParams{}, nil, nil, fmt.Errorf("unknown secret hash param %q", name)

internal/auth/password_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,10 @@ func TestGenerateSecretReturnsOpaqueValue(t *testing.T) {
3232
t.Fatalf("generated secret too short: %d", len(secret))
3333
}
3434
}
35+
36+
func TestVerifySecretRejectsParallelismOverflow(t *testing.T) {
37+
encoded := "$argon2id$v=19$m=65536,t=3,p=256$c2FsdHNhbHRzYWx0c2FsdA$AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
38+
if _, err := VerifySecret("secret", encoded); err == nil {
39+
t.Fatal("expected VerifySecret to reject p value that overflows uint8")
40+
}
41+
}

internal/config/config.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ type Config struct {
2020
}
2121

2222
type HTTPConfig struct {
23-
Address string `yaml:"address"`
24-
Port int `yaml:"port"`
23+
Address string `yaml:"address"`
24+
Port int `yaml:"port"`
25+
SecureCookies bool `yaml:"secureCookies"`
2526
}
2627

2728
type StorageConfig struct {
@@ -82,8 +83,9 @@ func (c *Config) applyEnvironment() {
8283
func Default() Config {
8384
return Config{
8485
HTTP: HTTPConfig{
85-
Address: "0.0.0.0",
86-
Port: 5000,
86+
Address: "0.0.0.0",
87+
Port: 5000,
88+
SecureCookies: true,
8789
},
8890
Storage: StorageConfig{
8991
RootDirectory: "/var/lib/scr/registry",

internal/config/config_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ func TestLoadDefaultsWhenConfigMissing(t *testing.T) {
1515
if cfg.HTTP.Port != 5000 {
1616
t.Fatalf("expected default port 5000, got %d", cfg.HTTP.Port)
1717
}
18+
if !cfg.HTTP.SecureCookies {
19+
t.Fatal("expected secure cookies to be enabled by default")
20+
}
1821
if cfg.Storage.RootDirectory != "/var/lib/scr/registry" {
1922
t.Fatalf("unexpected default storage root %q", cfg.Storage.RootDirectory)
2023
}
@@ -45,6 +48,7 @@ func TestLoadParsesDurationFields(t *testing.T) {
4548
http:
4649
address: "127.0.0.1"
4750
port: 5000
51+
secureCookies: false
4852
storage:
4953
rootDirectory: "/tmp/registry"
5054
gcDelay: "30m"
@@ -67,6 +71,9 @@ auth:
6771
if cfg.Auth.TokenTTL.Std() != 5*time.Minute {
6872
t.Fatalf("unexpected token ttl %s", cfg.Auth.TokenTTL.Std())
6973
}
74+
if cfg.HTTP.SecureCookies {
75+
t.Fatal("expected secure cookies to be configurable")
76+
}
7077
if cfg.Storage.GCDelay.Std() != 30*time.Minute {
7178
t.Fatalf("unexpected gc delay %s", cfg.Storage.GCDelay.Std())
7279
}

internal/httpserver/server.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,10 @@ func writeStorageError(w http.ResponseWriter, err error) {
772772
writeError(w, http.StatusBadRequest, "invalid digest")
773773
return
774774
}
775+
if errors.Is(err, storage.ErrInvalidReference) {
776+
writeError(w, http.StatusBadRequest, "invalid reference")
777+
return
778+
}
775779
if errors.Is(err, storage.ErrNotFound) {
776780
writeError(w, http.StatusNotFound, "not found")
777781
return

internal/httpserver/server_test.go

Lines changed: 80 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -530,9 +530,28 @@ func TestUILoginAndDashboard(t *testing.T) {
530530
if len(loginResponse.Result().Cookies()) == 0 {
531531
t.Fatal("expected session cookie")
532532
}
533+
sessionCookie := loginResponse.Result().Cookies()[0]
534+
if !sessionCookie.Secure || !sessionCookie.HttpOnly || sessionCookie.SameSite != http.SameSiteLaxMode {
535+
t.Fatalf("expected secure HttpOnly SameSite=Lax session cookie, got %#v", sessionCookie)
536+
}
537+
538+
logoutRequest := httptest.NewRequest(http.MethodPost, "/ui/logout", nil)
539+
logoutRequest.AddCookie(sessionCookie)
540+
logoutResponse := httptest.NewRecorder()
541+
handler.ServeHTTP(logoutResponse, logoutRequest)
542+
if logoutResponse.Code != http.StatusFound {
543+
t.Fatalf("expected logout redirect, got %d: %s", logoutResponse.Code, logoutResponse.Body.String())
544+
}
545+
if len(logoutResponse.Result().Cookies()) == 0 {
546+
t.Fatal("expected logout clearing cookie")
547+
}
548+
clearedCookie := logoutResponse.Result().Cookies()[0]
549+
if !clearedCookie.Secure || !clearedCookie.HttpOnly || clearedCookie.SameSite != http.SameSiteLaxMode || clearedCookie.MaxAge != -1 {
550+
t.Fatalf("expected secure clearing cookie, got %#v", clearedCookie)
551+
}
533552

534553
dashboardRequest := httptest.NewRequest(http.MethodGet, "/ui", nil)
535-
dashboardRequest.AddCookie(loginResponse.Result().Cookies()[0])
554+
dashboardRequest.AddCookie(sessionCookie)
536555
dashboardResponse := httptest.NewRecorder()
537556
handler.ServeHTTP(dashboardResponse, dashboardRequest)
538557
if dashboardResponse.Code != http.StatusOK {
@@ -549,7 +568,7 @@ func TestUILoginAndDashboard(t *testing.T) {
549568
}
550569

551570
settingsRequest := httptest.NewRequest(http.MethodGet, "/ui/settings", nil)
552-
settingsRequest.AddCookie(loginResponse.Result().Cookies()[0])
571+
settingsRequest.AddCookie(sessionCookie)
553572
settingsResponse := httptest.NewRecorder()
554573
handler.ServeHTTP(settingsResponse, settingsRequest)
555574
if settingsResponse.Code != http.StatusOK {
@@ -567,7 +586,7 @@ func TestUILoginAndDashboard(t *testing.T) {
567586
settingsForm.Set("interval", "2h")
568587
settingsUpdateRequest := httptest.NewRequest(http.MethodPost, "/ui/settings/gc", strings.NewReader(settingsForm.Encode()))
569588
settingsUpdateRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
570-
settingsUpdateRequest.AddCookie(loginResponse.Result().Cookies()[0])
589+
settingsUpdateRequest.AddCookie(sessionCookie)
571590
settingsUpdateResponse := httptest.NewRecorder()
572591
handler.ServeHTTP(settingsUpdateResponse, settingsUpdateRequest)
573592
if settingsUpdateResponse.Code != http.StatusFound {
@@ -584,7 +603,7 @@ func TestUILoginAndDashboard(t *testing.T) {
584603
webhookForm.Set("url", "https://example.com/scr-events")
585604
webhookUpdateRequest := httptest.NewRequest(http.MethodPost, "/ui/settings/webhook", strings.NewReader(webhookForm.Encode()))
586605
webhookUpdateRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
587-
webhookUpdateRequest.AddCookie(loginResponse.Result().Cookies()[0])
606+
webhookUpdateRequest.AddCookie(sessionCookie)
588607
webhookUpdateResponse := httptest.NewRecorder()
589608
handler.ServeHTTP(webhookUpdateResponse, webhookUpdateRequest)
590609
if webhookUpdateResponse.Code != http.StatusFound {
@@ -614,7 +633,7 @@ func TestUILoginAndDashboard(t *testing.T) {
614633
}
615634
}
616635
trafficRequest := httptest.NewRequest(http.MethodGet, "/ui?repository="+url.QueryEscape("ui/app"), nil)
617-
trafficRequest.AddCookie(loginResponse.Result().Cookies()[0])
636+
trafficRequest.AddCookie(sessionCookie)
618637
trafficResponse := httptest.NewRecorder()
619638
handler.ServeHTTP(trafficResponse, trafficRequest)
620639
if trafficResponse.Code != http.StatusOK {
@@ -627,7 +646,7 @@ func TestUILoginAndDashboard(t *testing.T) {
627646
t.Fatalf("expected filtered dashboard traffic for ui/app, got %s", trafficResponse.Body.String())
628647
}
629648
repositoriesRequest := httptest.NewRequest(http.MethodGet, "/ui/repositories", nil)
630-
repositoriesRequest.AddCookie(loginResponse.Result().Cookies()[0])
649+
repositoriesRequest.AddCookie(sessionCookie)
631650
repositoriesResponse := httptest.NewRecorder()
632651
handler.ServeHTTP(repositoriesResponse, repositoriesRequest)
633652
if repositoriesResponse.Code != http.StatusOK {
@@ -652,7 +671,7 @@ func TestUILoginAndDashboard(t *testing.T) {
652671
t.Fatalf("expected tag delete semantics copy, got %s", repositoriesResponse.Body.String())
653672
}
654673
searchRequest := httptest.NewRequest(http.MethodGet, "/ui/repositories?q=stable", nil)
655-
searchRequest.AddCookie(loginResponse.Result().Cookies()[0])
674+
searchRequest.AddCookie(sessionCookie)
656675
searchResponse := httptest.NewRecorder()
657676
handler.ServeHTTP(searchResponse, searchRequest)
658677
if searchResponse.Code != http.StatusOK {
@@ -662,7 +681,7 @@ func TestUILoginAndDashboard(t *testing.T) {
662681
t.Fatalf("expected repository search to preserve query and return tag match, got %s", searchResponse.Body.String())
663682
}
664683
missingSearchRequest := httptest.NewRequest(http.MethodGet, "/ui/repositories?q=no-such-tag", nil)
665-
missingSearchRequest.AddCookie(loginResponse.Result().Cookies()[0])
684+
missingSearchRequest.AddCookie(sessionCookie)
666685
missingSearchResponse := httptest.NewRecorder()
667686
handler.ServeHTTP(missingSearchResponse, missingSearchRequest)
668687
if missingSearchResponse.Code != http.StatusOK {
@@ -677,7 +696,7 @@ func TestUILoginAndDashboard(t *testing.T) {
677696
deleteTagForm.Set("tag", "latest")
678697
deleteTagRequest := httptest.NewRequest(http.MethodPost, "/ui/repositories/delete-tag", strings.NewReader(deleteTagForm.Encode()))
679698
deleteTagRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
680-
deleteTagRequest.AddCookie(loginResponse.Result().Cookies()[0])
699+
deleteTagRequest.AddCookie(sessionCookie)
681700
deleteTagResponse := httptest.NewRecorder()
682701
handler.ServeHTTP(deleteTagResponse, deleteTagRequest)
683702
if deleteTagResponse.Code != http.StatusFound {
@@ -696,7 +715,7 @@ func TestUILoginAndDashboard(t *testing.T) {
696715
deleteRepositoryForm.Set("repository", "ui/app")
697716
deleteRepositoryRequest := httptest.NewRequest(http.MethodPost, "/ui/repositories/delete", strings.NewReader(deleteRepositoryForm.Encode()))
698717
deleteRepositoryRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
699-
deleteRepositoryRequest.AddCookie(loginResponse.Result().Cookies()[0])
718+
deleteRepositoryRequest.AddCookie(sessionCookie)
700719
deleteRepositoryResponse := httptest.NewRecorder()
701720
handler.ServeHTTP(deleteRepositoryResponse, deleteRepositoryRequest)
702721
if deleteRepositoryResponse.Code != http.StatusFound {
@@ -714,7 +733,7 @@ func TestUILoginAndDashboard(t *testing.T) {
714733
createUserForm.Set("expiresAt", "2026-08-02")
715734
createUserRequest := httptest.NewRequest(http.MethodPost, "/ui/users", strings.NewReader(createUserForm.Encode()))
716735
createUserRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
717-
createUserRequest.AddCookie(loginResponse.Result().Cookies()[0])
736+
createUserRequest.AddCookie(sessionCookie)
718737
createUserResponse := httptest.NewRecorder()
719738
handler.ServeHTTP(createUserResponse, createUserRequest)
720739
if createUserResponse.Code != http.StatusOK {
@@ -725,7 +744,7 @@ func TestUILoginAndDashboard(t *testing.T) {
725744
}
726745

727746
usersRequest := httptest.NewRequest(http.MethodGet, "/ui/users", nil)
728-
usersRequest.AddCookie(loginResponse.Result().Cookies()[0])
747+
usersRequest.AddCookie(sessionCookie)
729748
usersResponse := httptest.NewRecorder()
730749
handler.ServeHTTP(usersResponse, usersRequest)
731750
if usersResponse.Code != http.StatusOK {
@@ -749,7 +768,7 @@ func TestUILoginAndDashboard(t *testing.T) {
749768
t.Fatalf("expected default wildcard pull grant, got %#v", grants)
750769
}
751770
usersWithGrantRequest := httptest.NewRequest(http.MethodGet, "/ui/users", nil)
752-
usersWithGrantRequest.AddCookie(loginResponse.Result().Cookies()[0])
771+
usersWithGrantRequest.AddCookie(sessionCookie)
753772
usersWithGrantResponse := httptest.NewRecorder()
754773
handler.ServeHTTP(usersWithGrantResponse, usersWithGrantRequest)
755774
if usersWithGrantResponse.Code != http.StatusOK {
@@ -774,7 +793,7 @@ func TestUILoginAndDashboard(t *testing.T) {
774793
validityForm.Add("actions", "push")
775794
validityRequest := httptest.NewRequest(http.MethodPost, "/ui/users/"+createdUser.ID+"/access", strings.NewReader(validityForm.Encode()))
776795
validityRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
777-
validityRequest.AddCookie(loginResponse.Result().Cookies()[0])
796+
validityRequest.AddCookie(sessionCookie)
778797
validityResponse := httptest.NewRecorder()
779798
handler.ServeHTTP(validityResponse, validityRequest)
780799
if validityResponse.Code != http.StatusFound {
@@ -800,7 +819,7 @@ func TestUILoginAndDashboard(t *testing.T) {
800819
}
801820
adminValidityRequest := httptest.NewRequest(http.MethodPost, "/ui/users/"+adminUser.ID+"/access", strings.NewReader(validityForm.Encode()))
802821
adminValidityRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
803-
adminValidityRequest.AddCookie(loginResponse.Result().Cookies()[0])
822+
adminValidityRequest.AddCookie(sessionCookie)
804823
adminValidityResponse := httptest.NewRecorder()
805824
handler.ServeHTTP(adminValidityResponse, adminValidityRequest)
806825
if adminValidityResponse.Code != http.StatusOK || !strings.Contains(adminValidityResponse.Body.String(), "Admin users are managed outside this user list") {
@@ -809,7 +828,7 @@ func TestUILoginAndDashboard(t *testing.T) {
809828

810829
deleteUser := createHTTPTestUser(t, ctx, store, "delete-me", "Delete Me", domain.RoleReader, "delete-secret", time.Now().UTC())
811830
deleteRequest := httptest.NewRequest(http.MethodPost, "/ui/users/"+deleteUser.ID+"/delete", nil)
812-
deleteRequest.AddCookie(loginResponse.Result().Cookies()[0])
831+
deleteRequest.AddCookie(sessionCookie)
813832
deleteResponse := httptest.NewRecorder()
814833
handler.ServeHTTP(deleteResponse, deleteRequest)
815834
if deleteResponse.Code != http.StatusFound {
@@ -820,7 +839,7 @@ func TestUILoginAndDashboard(t *testing.T) {
820839
}
821840

822841
auditRequest := httptest.NewRequest(http.MethodGet, "/ui/audit", nil)
823-
auditRequest.AddCookie(loginResponse.Result().Cookies()[0])
842+
auditRequest.AddCookie(sessionCookie)
824843
auditResponse := httptest.NewRecorder()
825844
handler.ServeHTTP(auditResponse, auditRequest)
826845
if auditResponse.Code != http.StatusOK {
@@ -834,7 +853,7 @@ func TestUILoginAndDashboard(t *testing.T) {
834853
}
835854

836855
auditSearchRequest := httptest.NewRequest(http.MethodGet, "/ui/audit?q=ui-reader", nil)
837-
auditSearchRequest.AddCookie(loginResponse.Result().Cookies()[0])
856+
auditSearchRequest.AddCookie(sessionCookie)
838857
auditSearchResponse := httptest.NewRecorder()
839858
handler.ServeHTTP(auditSearchResponse, auditSearchRequest)
840859
if auditSearchResponse.Code != http.StatusOK {
@@ -845,7 +864,7 @@ func TestUILoginAndDashboard(t *testing.T) {
845864
}
846865

847866
authFilterRequest := httptest.NewRequest(http.MethodGet, "/ui/audit?action=authentication", nil)
848-
authFilterRequest.AddCookie(loginResponse.Result().Cookies()[0])
867+
authFilterRequest.AddCookie(sessionCookie)
849868
authFilterResponse := httptest.NewRecorder()
850869
handler.ServeHTTP(authFilterResponse, authFilterRequest)
851870
if authFilterResponse.Code != http.StatusOK {
@@ -856,7 +875,7 @@ func TestUILoginAndDashboard(t *testing.T) {
856875
}
857876

858877
emptyAuditRequest := httptest.NewRequest(http.MethodGet, "/ui/audit?q=no-such-audit-event", nil)
859-
emptyAuditRequest.AddCookie(loginResponse.Result().Cookies()[0])
878+
emptyAuditRequest.AddCookie(sessionCookie)
860879
emptyAuditResponse := httptest.NewRecorder()
861880
handler.ServeHTTP(emptyAuditResponse, emptyAuditRequest)
862881
if emptyAuditResponse.Code != http.StatusOK {
@@ -999,6 +1018,47 @@ func TestRegistryWebhookFailureDoesNotFailRegistryRequest(t *testing.T) {
9991018
waitForWebhookAttempt(t, &attempts)
10001019
}
10011020

1021+
func TestUILoginCanDisableSecureCookieForDirectHTTP(t *testing.T) {
1022+
ctx := context.Background()
1023+
cfg := config.Default()
1024+
cfg.HTTP.SecureCookies = false
1025+
cfg.Storage.RootDirectory = filepath.Join(t.TempDir(), "registry")
1026+
cfg.Database.DSN = filepath.Join(t.TempDir(), "test.db")
1027+
store, err := db.Open(ctx, cfg.Database.DSN)
1028+
if err != nil {
1029+
t.Fatalf("Open() error = %v", err)
1030+
}
1031+
t.Cleanup(func() { _ = store.Close() })
1032+
if err := store.InitSchema(ctx); err != nil {
1033+
t.Fatalf("InitSchema() error = %v", err)
1034+
}
1035+
if err := store.EnsureActiveSigningKey(ctx); err != nil {
1036+
t.Fatalf("EnsureActiveSigningKey() error = %v", err)
1037+
}
1038+
if err := auth.BootstrapAdmin(ctx, store, "admin", "secret", time.Now().UTC()); err != nil {
1039+
t.Fatalf("BootstrapAdmin() error = %v", err)
1040+
}
1041+
handler := New(Options{Config: cfg, Store: store})
1042+
1043+
loginForm := url.Values{}
1044+
loginForm.Set("username", "admin")
1045+
loginForm.Set("password", "secret")
1046+
loginRequest := httptest.NewRequest(http.MethodPost, "/ui/login", strings.NewReader(loginForm.Encode()))
1047+
loginRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
1048+
loginResponse := httptest.NewRecorder()
1049+
handler.ServeHTTP(loginResponse, loginRequest)
1050+
if loginResponse.Code != http.StatusFound {
1051+
t.Fatalf("expected login redirect, got %d: %s", loginResponse.Code, loginResponse.Body.String())
1052+
}
1053+
if len(loginResponse.Result().Cookies()) == 0 {
1054+
t.Fatal("expected session cookie")
1055+
}
1056+
sessionCookie := loginResponse.Result().Cookies()[0]
1057+
if sessionCookie.Secure || !sessionCookie.HttpOnly || sessionCookie.SameSite != http.SameSiteLaxMode {
1058+
t.Fatalf("expected insecure opt-out to affect only Secure flag, got %#v", sessionCookie)
1059+
}
1060+
}
1061+
10021062
func TestRootRedirectsToRegistryAPI(t *testing.T) {
10031063
ctx := context.Background()
10041064
cfg := config.Default()

0 commit comments

Comments
 (0)