package main
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"html/template"
"net/http"
"net/url"
"strings"
)
// scriptHash is the CSP source expression pinning the shell's inline script.
// It's computed from the *rendered* shell rather than the raw shellScriptJS
// constant: html/template rewrites some characters (e.g. "<") inside a
// <script> element as breakout protection, so only the emitted bytes are what
// the browser actually hashes. Deriving it here means it can never drift from
// what's served.
var scriptHash = "sha256-" + func() string {
sum := sha256.Sum256([]byte(renderedShellScript()))
return base64.StdEncoding.EncodeToString(sum[:])
}()
// renderedShellScript executes the shell template and returns the exact bytes
// between <script> and </script> — the content the CSP hash must cover.
func renderedShellScript() string {
var buf bytes.Buffer
_ = shellTpl.Execute(&buf, struct {
Title, Domain, Active, Username, Version, Initials, SearchQuery, Lang, Theme, ThemeIcon, ThemeNext, NextPath string
LoggedIn, IsAdmin bool
Body, BrandMark, IconSprite template.HTML
}{Lang: "en"})
html := buf.String()
open := strings.Index(html, "<script>")
end := strings.Index(html, "</script>")
if open < 0 || end < 0 || end < open {
return ""
}
return html[open+len("<script>") : end]
}
// contentSecurityPolicy is deliberately strict: no 'unsafe-inline' for
// scripts (the one inline block is allowed by its hash), everything else
// same-origin, framing forbidden. style-src keeps 'unsafe-inline' because the
// templates rely on inline style="" attributes throughout — those carry no
// script and can't be nonce/hash-pinned, so this is the tightest workable
// policy without a template-wide refactor. img-src allows data: for the
// inline SVG favicons and markdown images.
func contentSecurityPolicy() string {
return strings.Join([]string{
"default-src 'self'",
"script-src 'self' '" + scriptHash + "'",
"style-src 'self' 'unsafe-inline'",
// https: lets README images/badges load; still blocks http: and
// other schemes. data: covers the inline SVG favicons.
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self'",
"object-src 'none'",
"base-uri 'none'",
"frame-ancestors 'none'",
"form-action 'self'",
}, "; ")
}
// securityHeaders sets the standard hardening response headers on every
// response. HSTS is safe to always emit: the app is only reachable over TLS
// in production (the ingress terminates it) and browsers ignore the header on
// plain-HTTP responses.
func securityHeaders(next http.Handler) http.Handler {
csp := contentSecurityPolicy()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
h.Set("Content-Security-Policy", csp)
h.Set("X-Frame-Options", "DENY")
h.Set("X-Content-Type-Options", "nosniff")
h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
h.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
next.ServeHTTP(w, r)
})
}
// sameOriginPOST rejects state-changing requests whose Origin (or, failing
// that, Referer) is not this same site. Combined with the SameSite=Lax
// session cookie, this is defense-in-depth against CSRF that needs no
// per-form token: browsers attach Origin to form POSTs, and a cross-site
// attacker cannot forge it. A request that carries neither header on an
// unsafe method is refused rather than trusted.
func (s *server) sameOriginPOST(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace:
next.ServeHTTP(w, r)
return
}
// The git-upload-pack POST (see handlers_git_http.go) is a real git
// client, not a browser — it never sends Origin/Referer, so it
// would always be refused otherwise. Exempting it isn't a CSRF
// hole: it carries no session cookie, mutates no state, and only
// ever reads a repo already re-checked as public.
if strings.HasSuffix(r.URL.Path, "/git-upload-pack") {
next.ServeHTTP(w, r)
return
}
if !sameOrigin(r) {
http.Error(w, "cross-origin request refused", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
func sameOrigin(r *http.Request) bool {
source := r.Header.Get("Origin")
if source == "" {
source = r.Header.Get("Referer")
}
if source == "" {
return false
}
u, err := url.Parse(source)
if err != nil || u.Host == "" {
return false
}
return strings.EqualFold(u.Host, r.Host)
}