feat(security): rate-limit /api/flows/start, CSRF double-submit cookie, CSP headers

- Rate-limit /api/flows/start to 10 req/min per user (in-memory, matches login pattern)
- Add middleware.ts: validates x-csrf-token header against csrf_token cookie on all
  API POST requests; issues the cookie on GET if missing; sets CSP, X-Frame-Options,
  X-Content-Type-Options, and Referrer-Policy on all responses
- Add lib/csrf.ts: client-side apiFetch() wrapper that injects the CSRF header
- Update all client components (login, useFlowRun, docker, caddy, git, systemd)
  to use apiFetch() for POST requests
- Cookie config in login route already correct (NODE_ENV check, httpOnly, sameSite=strict)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scrum4Me Agent 2026-05-13 20:01:43 +02:00
parent 1e31e3b584
commit aa1fd41bec
11 changed files with 108 additions and 8 deletions

View file

@ -1,9 +1,10 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import { apiFetch } from '@/lib/csrf'
async function fetchDiff(repoPath: string): Promise<string> {
const res = await fetch('/api/agent/exec', {
const res = await apiFetch('/api/agent/exec', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command_key: 'git_diff', args: [repoPath] }),

View file

@ -6,6 +6,7 @@ import { type RepoStatus, parseGitStatus } from '@/lib/parse-git'
import { useFlowRun } from '@/hooks/useFlowRun'
import ConfirmDialog from '@/components/ConfirmDialog'
import StreamingTerminal from '@/components/StreamingTerminal'
import { apiFetch } from '@/lib/csrf'
interface RepoEntry {
path: string
@ -15,7 +16,7 @@ interface RepoEntry {
}
async function fetchRepoStatus(repoPath: string): Promise<RepoStatus> {
const res = await fetch('/api/agent/exec', {
const res = await apiFetch('/api/agent/exec', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command_key: 'git_status', args: [repoPath] }),