Skip to content

Commit b628931

Browse files
Jeroen NijssenJeroen Nijssen
authored andcommitted
fix(auth): scope helper identity bypass to helper routes
1 parent 273a89b commit b628931

2 files changed

Lines changed: 67 additions & 10 deletions

File tree

backend/cmd/server/auth_middleware.go

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,11 @@ func (a *app) requireAuth(next http.Handler) http.Handler {
104104
next.ServeHTTP(w, r)
105105
return
106106
}
107-
// Helper protocol: requests carrying both identity headers
108-
// (X-RSM-Device-Type + X-RSM-Fingerprint) are deferred to the
109-
// downstream handler (authorizeHelperSyncRequest in helper_auth.go),
110-
// which enforces either app-password validation or the
111-
// auto-enroll-window policy. Pre-empting that decision here would
112-
// break the bootstrap flow where a fresh helper has no key yet.
113-
if hasHelperIdentity(r) {
107+
// Helper protocol: only endpoints that actually call
108+
// authorizeHelperSyncRequest may defer auth to the downstream handler.
109+
// Checking identity headers alone would let callers spoof those headers
110+
// and bypass AUTH_MODE on unrelated API/static routes.
111+
if isHelperProtocolRequest(r) {
114112
next.ServeHTTP(w, r)
115113
return
116114
}
@@ -126,10 +124,35 @@ func (a *app) requireAuth(next http.Handler) http.Handler {
126124
})
127125
}
128126

127+
// isHelperProtocolRequest reports whether r is one of the helper-facing routes
128+
// whose handler performs authorizeHelperSyncRequest. Identity headers alone are
129+
// not sufficient; non-helper routes must still require normal auth.
130+
func isHelperProtocolRequest(r *http.Request) bool {
131+
if !hasHelperIdentity(r) {
132+
return false
133+
}
134+
p := stripRoutePrefix(r.URL.Path)
135+
switch r.Method {
136+
case http.MethodGet:
137+
if p == "/save/latest" ||
138+
p == "/saves/download" ||
139+
p == "/saves/download_many" ||
140+
p == "/saves/download-many" {
141+
return true
142+
}
143+
return strings.HasPrefix(p, "/saves/") && strings.HasSuffix(p, "/download")
144+
case http.MethodPost:
145+
return p == "/saves" ||
146+
p == "/devices/config/report" ||
147+
p == "/helpers/config/sync" ||
148+
p == "/helpers/heartbeat"
149+
default:
150+
return false
151+
}
152+
}
153+
129154
// hasHelperIdentity reports whether r carries both helper-identity headers
130-
// (X-RSM-Device-Type + X-RSM-Fingerprint). The middleware uses this to
131-
// recognize the helper protocol and defer the auth decision to
132-
// authorizeHelperSyncRequest, which knows the auto-enroll-window policy.
155+
// (X-RSM-Device-Type + X-RSM-Fingerprint).
133156
func hasHelperIdentity(r *http.Request) bool {
134157
dt := strings.TrimSpace(r.Header.Get("X-RSM-Device-Type"))
135158
fp := strings.TrimSpace(r.Header.Get("X-RSM-Fingerprint"))

backend/cmd/server/auth_middleware_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,40 @@ func TestAuthMiddleware_Enabled_HelperHeadersValidKey_Allows(t *testing.T) {
217217
assertStatus(t, rr, http.StatusOK)
218218
}
219219

220+
func TestAuthMiddleware_Enabled_HelperHeadersOnProtectedListRoute_Rejects(t *testing.T) {
221+
h := authMiddlewareHarness(t, true)
222+
req := rawRequest(http.MethodGet, "/api/saves")
223+
req.Header.Set("X-RSM-Device-Type", "linux-x86")
224+
req.Header.Set("X-RSM-Fingerprint", "spoofed-helper-headers")
225+
rr := h.do(req)
226+
assertStatus(t, rr, http.StatusUnauthorized)
227+
}
228+
229+
func TestAuthMiddleware_Enabled_HelperHeadersOnStaticRoute_Rejects(t *testing.T) {
230+
h := staticFrontendHarness(t, true)
231+
req := rawRequest(http.MethodGet, "/")
232+
req.Header.Set("X-RSM-Device-Type", "linux-x86")
233+
req.Header.Set("X-RSM-Fingerprint", "spoofed-helper-headers")
234+
rr := h.do(req)
235+
assertStatus(t, rr, http.StatusUnauthorized)
236+
}
237+
238+
func TestAuthMiddleware_Enabled_HelperHeadersNoKey_AgentUpload_AutoEnrollOpen_Allows(t *testing.T) {
239+
h := authMiddlewareHarness(t, true)
240+
h.app.mu.Lock()
241+
h.app.enableAutoAppPasswordWindowLocked(15 * time.Minute)
242+
h.app.mu.Unlock()
243+
244+
req := helperMultipartSaveRequest(t, "linux-x86", "deck-middleware-agent-open", "")
245+
req.URL.Path = "/api/saves"
246+
req.RequestURI = "/api/saves"
247+
rr := h.do(req)
248+
assertStatus(t, rr, http.StatusOK)
249+
if got := rr.Header().Get("X-RSM-Auto-App-Password"); got == "" {
250+
t.Fatalf("expected auto-provisioned app password header on agent bootstrap; headers=%v", rr.Header())
251+
}
252+
}
253+
220254
// Regression test for the GET /api/saves + valid Bearer path. The empirical
221255
// repro that surfaced the bootstrap bug had an empty Bearer (enrollment had
222256
// failed), so this confirms the happy path independently.

0 commit comments

Comments
 (0)