From c8b9d473a709dfaa2bed7b52f31cc1f34fcdd00e Mon Sep 17 00:00:00 2001 From: simpleqt <89645338+simpleqt@users.noreply.github.com> Date: Mon, 24 Aug 2026 11:42:06 +0800 Subject: [PATCH] fix(deploy): strip internal trust headers at proxy layer; run containers as non-root Proxy headers: the Gateway treats X-DeerFlow-Internal-Token and X-DeerFlow-Owner-User-Id as internal credentials that bypass session auth and impersonate arbitrary users. Browser clients can set arbitrary custom headers, so the public hops (Next.js /api/memory proxy and every nginx proxying location) now strip both headers before forwarding. nginx does not inherit proxy_set_header into locations that define their own, hence the explicit clearing in all 16 locations. Containers: the frontend, backend and provisioner images all ran as root. The agent runtime can execute tool/shell commands, so a container compromise (or a prompt-injected run in host-bash mode) meant instant root. The frontend now runs as the image's uid-1000 node user, the backend and provisioner create a dedicated uid-1000 deerflow user, and HOME is set so ~/.deer-flow runtime state stays writable. The DooD overlay opts the gateway back into root explicitly because the host Docker socket requires it; this matches the Helm chart, which already sets runAsNonRoot/runAsUser 1000. --- backend/Dockerfile | 8 ++++ docker/docker-compose.dood.yaml | 4 ++ docker/nginx/nginx.conf | 38 +++++++++++++++++++ docker/provisioner/Dockerfile | 3 ++ frontend/Dockerfile | 2 + .../src/app/api/memory/[...path]/route.ts | 5 +++ frontend/src/app/api/memory/route.ts | 5 +++ 7 files changed, 65 insertions(+) diff --git a/backend/Dockerfile b/backend/Dockerfile index b1b17595b46..0c80988a4bf 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -120,6 +120,14 @@ WORKDIR /app # Copy backend with pre-built virtualenv from builder COPY --from=builder /app/backend ./backend +# Run as an unprivileged user. The Gateway writes runtime state under +# ~/.deer-flow, so keep HOME writable for this user. Deployments that need +# the DooD sandbox overlay can opt back into root via `user: root` in +# docker-compose (see docker/docker-compose.dood.yaml). +RUN useradd --create-home --uid 1000 deerflow +USER deerflow +ENV HOME=/home/deerflow + # Expose Gateway API port. EXPOSE 8001 diff --git a/docker/docker-compose.dood.yaml b/docker/docker-compose.dood.yaml index d353407c3df..42cc250a321 100644 --- a/docker/docker-compose.dood.yaml +++ b/docker/docker-compose.dood.yaml @@ -22,5 +22,9 @@ # onto it. DEER_FLOW_DOCKER_SOCKET defaults to /var/run/docker.sock. services: gateway: + # The base image now runs as an unprivileged user; talking to the host + # Docker socket (root:root by default) requires root, so this overlay + # opts the gateway back in explicitly. + user: root volumes: - ${DEER_FLOW_DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf index 61f97a9248b..988cdb78d5e 100644 --- a/docker/nginx/nginx.conf +++ b/docker/nginx/nginx.conf @@ -69,6 +69,12 @@ http { proxy_buffering off; proxy_cache off; + # The Gateway trusts X-DeerFlow-Internal-Token / X-DeerFlow-Owner-User-Id + # as internal credentials that bypass session auth. nginx does not + # inherit proxy_set_header into locations that define their own, so + # every proxying location below clears these two headers explicitly + # to keep browser-supplied values from ever reaching the Gateway. + # Keep the unified nginx endpoint same-origin by default. When split # frontend/backend or port-forwarded deployments need browser CORS, # configure the Gateway allowlist with GATEWAY_CORS_ORIGINS so CORS and @@ -84,6 +90,8 @@ http { # Headers proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -117,6 +125,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -128,6 +138,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -139,6 +151,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -150,6 +164,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -161,6 +177,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -172,6 +190,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -190,6 +210,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -208,6 +230,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -219,6 +243,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -230,6 +256,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -241,6 +269,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -252,6 +282,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -266,6 +298,8 @@ http { proxy_pass http://$provisioner_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -278,6 +312,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -293,6 +329,8 @@ http { # Headers proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; diff --git a/docker/provisioner/Dockerfile b/docker/provisioner/Dockerfile index 96ef93156f9..ca43435c401 100644 --- a/docker/provisioner/Dockerfile +++ b/docker/provisioner/Dockerfile @@ -24,6 +24,9 @@ RUN pip install --no-cache-dir \ WORKDIR /app COPY app.py . +RUN useradd --create-home --uid 1000 deerflow +USER deerflow + EXPOSE 8002 CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8002"] diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 4c6b22123c3..b4998461ce0 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -51,5 +51,7 @@ RUN pnpm config set store-dir ${PNPM_STORE_PATH} RUN if [ -n "${NPM_REGISTRY}" ]; then pnpm config set registry "${NPM_REGISTRY}"; fi WORKDIR /app COPY --from=builder /app/frontend ./frontend +# Run as the unprivileged `node` user (uid/gid 1000) shipped with the image. +USER node EXPOSE 3000 CMD ["sh", "-c", "cd /app/frontend && pnpm start"] diff --git a/frontend/src/app/api/memory/[...path]/route.ts b/frontend/src/app/api/memory/[...path]/route.ts index 8de48d7425c..0923c66f038 100644 --- a/frontend/src/app/api/memory/[...path]/route.ts +++ b/frontend/src/app/api/memory/[...path]/route.ts @@ -12,6 +12,11 @@ async function proxyRequest(request: NextRequest, pathname: string) { headers.delete("host"); headers.delete("connection"); headers.delete("content-length"); + // The gateway treats these as internal trust credentials that bypass + // session auth; browser clients can set arbitrary custom headers, so a + // request transiting this public proxy must never forward them. + headers.delete("x-deerflow-internal-token"); + headers.delete("x-deerflow-owner-user-id"); const hasBody = !["GET", "HEAD"].includes(request.method); const response = await fetch(buildBackendUrl(pathname), { diff --git a/frontend/src/app/api/memory/route.ts b/frontend/src/app/api/memory/route.ts index 2957c69c1a1..00a3e73a567 100644 --- a/frontend/src/app/api/memory/route.ts +++ b/frontend/src/app/api/memory/route.ts @@ -12,6 +12,11 @@ async function proxyRequest(request: NextRequest, pathname: string) { headers.delete("host"); headers.delete("connection"); headers.delete("content-length"); + // The gateway treats these as internal trust credentials that bypass + // session auth; browser clients can set arbitrary custom headers, so a + // request transiting this public proxy must never forward them. + headers.delete("x-deerflow-internal-token"); + headers.delete("x-deerflow-owner-user-id"); const hasBody = !["GET", "HEAD"].includes(request.method); const response = await fetch(buildBackendUrl(pathname), {