oficina / docs Public

Allow the registered Home form destination and stack account details

Paul Campbell pushed by paul@keymail.dev 1e862f80752004d16db98693c25d4c693dc19510 parent 3ed7665
4 files changed, +73 −2
  • internal/docs/home_routes.go +3 −1
  • internal/docs/home_security.go +9 −0
  • internal/docs/home_security_test.go +60 −0
  • internal/docs/views/home_link.html +1 −1

CI passed — run details

diff --git a/internal/docs/home_routes.go b/internal/docs/home_routes.go
index 01f05de..7f93cd0 100644
--- a/internal/docs/home_routes.go
+++ b/internal/docs/home_routes.go
@@ -20,6 +20,7 @@ type homeRoutes struct {
bridge *homeBridge
navigation *localLinkNavigation
handlers *handlers
+ formPolicy string
}
func newHomeRoutes(settings *homeSettings, origin string, db *sql.DB, auth *rauth.Auth, h *handlers) (*homeRoutes, error) {
@@ -36,7 +37,7 @@ func newHomeRoutes(settings *homeSettings, origin string, db *sql.DB, auth *raut
return nil, errHomeConfig
}
b.returnPath = navigation.returnPath
- return &homeRoutes{bridge: b, navigation: navigation, handlers: h}, nil
+ return &homeRoutes{bridge: b, navigation: navigation, handlers: h, formPolicy: homeFormPolicy(settings.issuer)}, nil
}
func cookieCount(r *http.Request, name string) int {
@@ -59,6 +60,7 @@ func homeCSRF(origin string, home *homeRoutes) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
checked := protect(next)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Security-Policy", home.formPolicy)
if r.Host != strings.TrimPrefix(origin, "https://") {
http.NotFound(w, r)
return
diff --git a/internal/docs/home_security.go b/internal/docs/home_security.go
new file mode 100644
index 0000000..5ef7b0d
--- /dev/null
+++ b/internal/docs/home_security.go
@@ -0,0 +1,9 @@
+package docs
+
+// Browsers enforce form-action through a form's redirect chain. Linking starts
+// with a local POST and reaches Home; allow only the validated credential origin.
+// The drift test pins every other directive to the framework's actual default.
+func homeFormPolicy(issuer string) string {
+ return "default-src 'self'; style-src 'self' 'unsafe-inline'; " +
+ "img-src 'self' data:; frame-ancestors 'none'; base-uri 'self'; form-action 'self' " + issuer
+}
diff --git a/internal/docs/home_security_test.go b/internal/docs/home_security_test.go
new file mode 100644
index 0000000..7644f1b
--- /dev/null
+++ b/internal/docs/home_security_test.go
@@ -0,0 +1,60 @@
+package docs
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/rastrillo"
+)
+
+func TestHomeFormPolicyPreservesFrameworkAndStandalone(t *testing.T) {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })
+ baselineHandler, cleanup, err := rastrillo.Handler(rastrillo.Options{Mux: mux})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if cleanup != nil {
+ defer cleanup()
+ }
+ rec := httptest.NewRecorder()
+ baselineHandler.ServeHTTP(rec, httptest.NewRequest("GET", bridgeOrigin+"/", nil))
+ baseline := rec.Header().Get("Content-Security-Policy")
+ want := strings.Replace(baseline, "form-action 'self'", "form-action 'self' "+bridgeIssuer, 1)
+ if want == baseline {
+ t.Fatal("framework form-action changed; review policy")
+ }
+ f := newRouterHomeFixture(t)
+ wrapped, done, err := rastrillo.Handler(rastrillo.Options{Mux: f.app.Handler()})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if done != nil {
+ defer done()
+ }
+ for _, path := range []string{"/signin/local", "/account/home", "/auth/start", "/signed-out"} {
+ w := httptest.NewRecorder()
+ wrapped.ServeHTTP(w, httptest.NewRequest("GET", bridgeOrigin+path, nil))
+ if got := w.Header().Get("Content-Security-Policy"); got != want {
+ t.Fatalf("policy drift at %s: %q", path, got)
+ }
+ }
+ app, err := NewForTestWith(t, Config{Origin: bridgeOrigin, SampleDocuments: "0"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ standalone, closeStandalone, err := rastrillo.Handler(rastrillo.Options{Mux: app.Handler()})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if closeStandalone != nil {
+ defer closeStandalone()
+ }
+ w := httptest.NewRecorder()
+ standalone.ServeHTTP(w, httptest.NewRequest("GET", bridgeOrigin+"/signin", nil))
+ if got := w.Header().Get("Content-Security-Policy"); got != baseline {
+ t.Fatal("standalone policy changed")
+ }
+}
diff --git a/internal/docs/views/home_link.html b/internal/docs/views/home_link.html
index d5b1e02..6da4860 100644
--- a/internal/docs/views/home_link.html
+++ b/internal/docs/views/home_link.html
@@ -2,7 +2,7 @@
{{define "content"}}
<header rst-page-header><div rst-page-header-titles><h1>{{.Title}}</h1></div></header>
<div rst-box>
- <div rst-box-head><p>{{.Body}}</p>{{if .Address}}<p><strong>{{.Address}}</strong></p>{{end}}</div>
+ <p>{{.Body}}</p>{{if .Address}}<p style="overflow-wrap: anywhere"><strong>{{.Address}}</strong></p>{{end}}
{{if .PostAction}}<form method="post" action="{{.Action}}"><button rst-btn="primary" type="submit">{{.ActionLabel}}</button></form>
{{else}}<a rst-btn="primary" href="{{.Action}}">{{.ActionLabel}}</a>{{end}}
{{if .CancelLabel}}<a href="/">{{.CancelLabel}}</a>{{end}}