Allow the configured directory in credential form redirects
4 files changed,
+53
−7
cmd/home/main.go+1 −0internal/home/noscript_test.go+6 −7internal/home/security.go+8 −0internal/home/security_test.go+38 −0
CI passed — run details
diff --git a/cmd/home/main.go b/cmd/home/main.go| index 355cb9e..de742d6 100644 |
| --- a/cmd/home/main.go |
| +++ b/cmd/home/main.go |
| @@ -112,6 +112,7 @@ func main() { |
| mux := http.NewServeMux() |
| mux.Handle("/", app.Router()) |
| opts.Mux = mux |
| + opts.CSP = app.SecurityPolicy() |
| // Blanked because this file opened the handle itself: Serve would |
| // otherwise open a second one against the same file. |
diff --git a/internal/home/noscript_test.go b/internal/home/noscript_test.go| index 1b3055b..e60e27c 100644 |
| --- a/internal/home/noscript_test.go |
| +++ b/internal/home/noscript_test.go |
| @@ -232,7 +232,7 @@ func TestServedCSPAllowsNoInlineScript(t *testing.T) { |
| mux := http.NewServeMux() |
| mux.Handle("/", app.Router()) |
| - h, cleanup, err := rastrillo.Handler(rastrillo.Options{Mux: mux, Logger: quietLogger()}) |
| + h, cleanup, err := rastrillo.Handler(rastrillo.Options{Mux: mux, Logger: quietLogger(), CSP: app.SecurityPolicy()}) |
| if err != nil { |
| t.Fatalf("rastrillo.Handler: %v", err) |
| } |
| @@ -338,16 +338,15 @@ func TestServedCSPAllowsNoInlineScript(t *testing.T) { |
| } |
| } |
| -// The app must not replace the CSP at all. Setting Options.CSP is the |
| -// one move that turns the carve-out into a blanket exemption, and it |
| -// would not show up in any rendered page. |
| -func TestAppNeverSetsItsOwnCSP(t *testing.T) { |
| +// The binary uses the policy whose only framework change is tested separately. |
| +func TestAppUsesReviewedCSP(t *testing.T) { |
| b, err := os.ReadFile("../../cmd/home/main.go") |
| if err != nil { |
| t.Fatal(err) |
| } |
| - if bytes.Contains(b, []byte("CSP")) { |
| - t.Error("cmd/home/main.go mentions CSP; the default is what ruling 21 assumes") |
| + assignment := []byte("opts.CSP = app.SecurityPolicy()") |
| + if bytes.Count(b, assignment) != 1 || bytes.Contains(bytes.ReplaceAll(b, assignment, nil), []byte("CSP")) { |
| + t.Error("binary must wire exactly the reviewed policy") |
| } |
| } |
diff --git a/internal/home/security.go b/internal/home/security.go| new file mode 100644 |
| index 0000000..21fb5fc |
| --- /dev/null |
| +++ b/internal/home/security.go |
| @@ -0,0 +1,8 @@ |
| +package home |
| + |
| +// SecurityPolicy preserves the framework policy while allowing the configured |
| +// directory origin at the end of a credential form's redirect chain. |
| +func (a *App) SecurityPolicy() string { |
| + return "default-src 'self'; style-src 'self' 'unsafe-inline'; " + |
| + "img-src 'self' data:; frame-ancestors 'none'; base-uri 'self'; form-action 'self' " + a.origins.Directory |
| +} |
diff --git a/internal/home/security_test.go b/internal/home/security_test.go| new file mode 100644 |
| index 0000000..e166f32 |
| --- /dev/null |
| +++ b/internal/home/security_test.go |
| @@ -0,0 +1,38 @@ |
| +package home |
| + |
| +import ( |
| + "net/http" |
| + "net/http/httptest" |
| + "strings" |
| + "testing" |
| + |
| + "github.com/carlosframework/rastrillo" |
| +) |
| + |
| +func TestSecurityPolicyPreservesFrameworkDefaults(t *testing.T) { |
| + mux := http.NewServeMux() |
| + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }) |
| + handler, cleanup, err := rastrillo.Handler(rastrillo.Options{Mux: mux, Logger: quietLogger()}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if cleanup != nil { |
| + defer cleanup() |
| + } |
| + rec := httptest.NewRecorder() |
| + handler.ServeHTTP(rec, httptest.NewRequest("GET", "https://id.example.test/", nil)) |
| + baseline := rec.Header().Get("Content-Security-Policy") |
| + for _, origins := range []Origins{{Origin, directoryOrigin}, {"https://id.example.test", "https://directory.example.test"}} { |
| + if err := origins.validate(); err != nil { |
| + t.Fatal(err) |
| + } |
| + app := &App{origins: origins} |
| + want := strings.Replace(baseline, "form-action 'self'", "form-action 'self' "+origins.Directory, 1) |
| + if baseline == want { |
| + t.Fatal("framework form-action changed; review policy") |
| + } |
| + if got := app.SecurityPolicy(); got != want { |
| + t.Fatalf("policy differs beyond directory permission: %q", got) |
| + } |
| + } |
| +} |