aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/server.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/web/server.go')
-rw-r--r--internal/web/server.go34
1 files changed, 31 insertions, 3 deletions
diff --git a/internal/web/server.go b/internal/web/server.go
index 5a6ac4e..6009310 100644
--- a/internal/web/server.go
+++ b/internal/web/server.go
@@ -43,12 +43,16 @@ func NewServer(cfg *config.Config, st *store.Store) *Server {
// site plus the superadmin's root blog, one label below it is a blog, anything
// else is a 404.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if s.cfg.HTTPS { // every subdomain is ours, so the whole site may pin https
- w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
- }
+ s.secureHeaders(w, r)
host := hostname(r.Host)
switch {
case host == s.cfg.BaseDomain, host == config.RootSubdomain+"."+s.cfg.BaseDomain:
+ if managementPath(r.URL.Path) {
+ // Nothing may frame the dashboard or send its forms elsewhere. No
+ // script-src: its inline scripts are a product constraint.
+ w.Header().Set("X-Frame-Options", "DENY")
+ w.Header().Set("Content-Security-Policy", "frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'")
+ }
// The root blog is looked up lazily by hostBlog so management pages work even without one.
s.root.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxHostSub, config.RootSubdomain)))
case strings.HasSuffix(host, "."+s.cfg.BaseDomain):
@@ -63,6 +67,30 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
+// secureHeaders is what every response carries. Public blog pages get no
+// framing rule: they are the owner's content and may be embedded on purpose.
+func (s *Server) secureHeaders(w http.ResponseWriter, r *http.Request) {
+ h := w.Header()
+ h.Set("X-Content-Type-Options", "nosniff")
+ h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
+ if s.cfg.HTTPS { // every subdomain is ours, so the whole site may pin https
+ h.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
+ }
+}
+
+// managementPaths are the root-domain prefixes of the login and dashboard
+// pages (everything else on that host is the root blog).
+var managementPaths = []string{"/webadmin", "/logout", "/dashboard", "/account/", "/b/", "/admin/"}
+
+func managementPath(p string) bool {
+ for _, m := range managementPaths {
+ if strings.HasPrefix(p, m) {
+ return true
+ }
+ }
+ return false
+}
+
func hostname(h string) string {
if host, _, err := net.SplitHostPort(h); err == nil {
h = host