Skip to content

Commit 9a4f069

Browse files
Harden HTTP security boundaries
1 parent b609ec9 commit 9a4f069

1 file changed

Lines changed: 36 additions & 1 deletion

File tree

‎cmd/stackhost/main.go‎

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"path/filepath"
1616
"strconv"
1717
"strings"
18+
"sync"
1819
"syscall"
1920
"time"
2021

@@ -28,6 +29,12 @@ type app struct {
2829
sessionSecret string
2930
events chan map[string]any
3031
docker *dockerreader.Reader
32+
loginMu sync.Mutex
33+
loginAttempts map[string]attempt
34+
}
35+
type attempt struct {
36+
count int
37+
since time.Time
3138
}
3239

3340
func main() {
@@ -45,7 +52,7 @@ func main() {
4552
panic(err)
4653
}
4754
dr, _ := dockerreader.NewReader()
48-
a := &app{db: db, sessionSecret: getenv("STACKHOST_SESSION_SECRET", "development-only-change-me"), events: make(chan map[string]any, 32), docker: dr}
55+
a := &app{db: db, sessionSecret: getenv("STACKHOST_SESSION_SECRET", "development-only-change-me"), events: make(chan map[string]any, 32), docker: dr, loginAttempts: make(map[string]attempt)}
4956
s := &http.Server{Addr: addr, Handler: a.routes(), ReadHeaderTimeout: 10 * time.Second}
5057
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
5158
defer stop()
@@ -99,6 +106,17 @@ func security(next http.Handler) http.Handler {
99106
w.Header().Set("X-Content-Type-Options", "nosniff")
100107
w.Header().Set("X-Frame-Options", "DENY")
101108
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
109+
w.Header().Set("X-XSS-Protection", "0")
110+
w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
111+
if origin := os.Getenv("STACKHOST_ALLOWED_ORIGIN"); origin != "" {
112+
w.Header().Set("Access-Control-Allow-Origin", origin)
113+
w.Header().Set("Access-Control-Allow-Credentials", "true")
114+
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
115+
if r.Method == http.MethodOptions {
116+
w.WriteHeader(http.StatusNoContent)
117+
return
118+
}
119+
}
102120
next.ServeHTTP(w, r)
103121
})
104122
}
@@ -157,6 +175,10 @@ func (a *app) login(w http.ResponseWriter, r *http.Request) {
157175
jsonError(w, 422, "validation_failed", "Revise os campos informados.")
158176
return
159177
}
178+
if !a.loginAllowed(r) {
179+
jsonError(w, 429, "too_many_attempts", "Tente novamente em alguns instantes.")
180+
return
181+
}
160182
var id int64
161183
var hash string
162184
err := a.db.QueryRow("SELECT id,password_hash FROM users WHERE email=?", strings.ToLower(strings.TrimSpace(in.Email))).Scan(&id, &hash)
@@ -169,6 +191,19 @@ func (a *app) login(w http.ResponseWriter, r *http.Request) {
169191
a.createSession(w, r, id)
170192
json.NewEncoder(w).Encode(map[string]bool{"ok": true})
171193
}
194+
func (a *app) loginAllowed(r *http.Request) bool {
195+
ip := strings.Split(r.RemoteAddr, ":")[0]
196+
now := time.Now()
197+
a.loginMu.Lock()
198+
defer a.loginMu.Unlock()
199+
v := a.loginAttempts[ip]
200+
if now.Sub(v.since) > time.Minute {
201+
v = attempt{since: now}
202+
}
203+
v.count++
204+
a.loginAttempts[ip] = v
205+
return v.count <= 10
206+
}
172207
func (a *app) createSession(w http.ResponseWriter, r *http.Request, id int64) {
173208
seed := make([]byte, 32)
174209
if _, err := rand.Read(seed); err != nil {

0 commit comments

Comments
 (0)