返回 slidev
018-server-side-remote-auth.md
根目录 / plans / 018-server-side-remote-auth.md
1 # Plan 018: Enforce the `--remote` secret on the server, not just the client router
2
3 > **Executor instructions**: This is a larger security *design + implementation*
4 > plan touching the dev-server trust boundary. Read it fully first. Follow the
5 > steps, run every verification, and honor the STOP conditions — several ask you
6 > to pause and confirm the approach before writing broad changes. When done,
7 > update the status row in `plans/README.md`.
8 >
9 > **Drift check (run first)**: `git diff --stat c63cb120..HEAD -- packages/client/setup/routes.ts packages/slidev/node/vite/loaders.ts packages/slidev/node/vite/monacoWrite.ts packages/slidev/node/vite/serverRef.ts`
10 > On a mismatch with the excerpts below, treat it as a STOP condition.
11
12 ## Status
13
14 - **Priority**: P3 (high value, larger lift — schedule deliberately)
15 - **Effort**: M-L
16 - **Risk**: MED
17 - **Depends on**: none; **subsumes** the network-exposure parts of 015/017
18 (still land 015's path validation as defense-in-depth)
19 - **Category**: security
20 - **Planned at**: commit `c63cb120`, 2026-07-10
21
22 ## Why this matters
23
24 The `--remote <password>` feature's only access gate is a **client-side** Vue
25 Router `beforeEnter` guard. The dev server's privileged surface has no
26 server-side check:
27
28 - `GET /__slidev/slides/<n>.json` returns full slide content **and rendered
29 speaker notes**.
30 - `POST /__slidev/slides/<n>.json` mutates slide content/frontmatter/notes and
31 **persists `slides.md` to disk** (`parser.save`).
32 - ws handlers write files (Monaco) and persist drawings/snapshots.
33
34 So anyone who can reach the server — the LAN when `--remote` binds `0.0.0.0`, the
35 internet when `--tunnel` is added, or a malicious page via CSRF in default
36 localhost mode — can read private notes and overwrite the presenter's deck,
37 bypassing the password prompt entirely (it only hides SPA routes). This plan adds
38 a real server-side authorization boundary.
39
40 ## Current state
41
42 - Client-only guard — `packages/client/setup/routes.ts:8-20`:
43 ```ts
44 function passwordGuard(to: RouteLocationNormalized) {
45 if (!configs.remote || configs.remote === to.query.password) return true
46 if (configs.remote && to.query.password === undefined) {
47 const password = prompt('Enter password')
48 if (configs.remote === password) return true
49 }
50 // redirect away
51 }
52 ```
53 Applied only as `beforeEnter` on presenter/notes/print/export routes.
54 - Unauthenticated privileged HTTP middleware — `packages/slidev/node/vite/loaders.ts:79-141`
55 (GET returns `withRenderedNote(data.slides[idx])`; POST mutates + `parser.save`).
56 - Unauthenticated ws sinks — `vite/monacoWrite.ts:23-32`, `vite/serverRef.ts:28-36`.
57 - The shared secret is `configs.remote` (the `--remote` password), already
58 surfaced to the client via the `#slidev/configs` virtual module.
59
60 ## Design decisions to confirm BEFORE broad implementation
61
62 Use the `question` flow or report back to the operator on these (each has a
63 recommended default). **Do not** write the full change until these are settled:
64
65 1. **Scope of enforcement** — recommended: require auth on all **mutating**
66 endpoints (POST slides, monaco-write, drawings/snapshot persist) and on
67 **notes** content always; gate read of slide JSON only when `remote` is set.
68 2. **Credential mechanism** — recommended: a per-session token minted by the
69 server on startup (not the raw password), delivered to the trusted client via
70 the `#slidev/configs` virtual module, sent on each privileged request
71 (`Authorization: Bearer` / a header for HTTP, a field in ws messages). The
72 `--remote` password remains the human gate to *obtain* a session.
73 3. **Default-localhost behavior** — recommended: keep mutation enabled locally
74 but require the token (defeats CSRF); do not silently disable the editor.
75 4. **Backward compatibility** — recommended: when neither `remote` nor editor
76 features are enabled, behavior is unchanged for read paths.
77
78 ## Commands you will need
79
80 | Purpose | Command | Expected |
81 |---------|---------|----------|
82 | Install | `pnpm install` | exit 0 |
83 | Build | `pnpm build` | exit 0 |
84 | Typecheck | `pnpm typecheck` | exit 0 |
85 | Lint | `pnpm lint` | exit 0 |
86 | Test | `pnpm test` | pass (add auth-helper tests) |
87
88 ## Scope
89
90 **In scope** (after design confirmation):
91 - `packages/slidev/node/vite/loaders.ts` — auth check in the `/__slidev/*` middleware
92 - `packages/slidev/node/vite/monacoWrite.ts`, `vite/serverRef.ts` — auth on privileged ws messages
93 - Token minting/plumbing: a new small module (e.g. `node/auth.ts`) + the config
94 virtual module (`node/virtual/configs.ts`) to expose the token to the client
95 - `packages/client/**` editor/presenter/remote code that calls these endpoints,
96 to send the token
97 - A unit test for the token verify helper
98
99 **Out of scope**:
100 - Redesigning the presenter/remote UX beyond passing a token.
101 - TLS/transport security (that's the tunnel/user's responsibility).
102 - The path-traversal validations (plan 015) — land those independently.
103
104 ## Git workflow
105
106 - Branch: `feat/server-side-remote-auth`.
107 - Conventional commit(s): `feat(security): authorize privileged dev-server endpoints`.
108 - Do NOT push/PR unless instructed. This is a security-sensitive change — expect
109 review.
110
111 ## Steps
112
113 ### Step 1: Confirm the design (STOP-gated)
114
115 Resolve the four decisions above with the operator. Record the chosen answers at
116 the top of the branch's first commit message or in a short note. **STOP** and ask
117 if any answer is unclear — do not improvise a security protocol.
118
119 ### Step 2: Mint and expose a session token
120
121 Add `node/auth.ts` that, when `remote`/editor is enabled, generates a random
122 token at server start (`crypto.randomUUID()` or `crypto.randomBytes`). Expose it
123 to the trusted client through the config virtual module so the app can attach it;
124 never log the token value.
125
126 ### Step 3: Enforce on the HTTP middleware
127
128 In `loaders.ts`, before serving/ mutating in the `/__slidev/*` handler, verify
129 the token per the confirmed scope (Step 1 decision 1). Return `401` when missing/
130 wrong. Keep the bounds check from plan 011 if present.
131
132 ### Step 4: Enforce on the ws sinks
133
134 Require the token field in `slidev:monaco-write` and in the server-ref persistence
135 path; reject otherwise. Combine with plan 017's origin check if that landed.
136
137 ### Step 5: Update the client callers
138
139 Make the editor save, presenter sync, and remote-control code send the token with
140 each privileged call. Verify HMR and normal navigation are unaffected.
141
142 ### Step 6: Unit-test the verifier
143
144 Test the pure token-compare helper (constant-time compare if feasible) for
145 match/mismatch/missing.
146
147 **Verify**: `pnpm build && pnpm typecheck && pnpm lint && pnpm test` all pass;
148 manual check with `pnpm demo:dev` that editing/notes still work in-app and that a
149 request without the token to `POST /__slidev/slides/1.json` is rejected (`401`).
150
151 ## Test plan
152
153 - Unit-test the token verifier (deterministic).
154 - Manual/integration: in-app editor save works; an unauthenticated
155 `POST /__slidev/slides/<n>.json` and an unauthenticated `monaco-write` are
156 rejected. If plan 022 introduces a server test harness, add these as automated
157 cases.
158
159 ## Done criteria
160
161 - [ ] Design decisions (Step 1) are recorded
162 - [ ] Privileged HTTP endpoints reject unauthenticated requests (per chosen scope)
163 - [ ] Privileged ws handlers reject unauthenticated messages
164 - [ ] The trusted client attaches the token; in-app editor/notes/presenter still work
165 - [ ] Token value is never logged or written to disk in plaintext logs
166 - [ ] Token verifier is unit-tested
167 - [ ] `pnpm build && pnpm typecheck && pnpm lint && pnpm test` pass
168 - [ ] `plans/README.md` status row updated
169
170 ## STOP conditions
171
172 Stop and report (do not improvise) if:
173
174 - Any Step 1 decision is unresolved.
175 - Enforcing auth breaks HMR, cross-device remote control, or the browser
176 exporter in a way that can't be fixed by passing the token.
177 - The change starts sprawling beyond the in-scope files (e.g. requiring a new
178 transport) — re-scope with the operator.
179
180 ## Maintenance notes
181
182 - Treat the client `passwordGuard` as cosmetic UX after this lands; the server is
183 the real boundary.
184 - Keep plan 015 (path validation) and 017 (origin) as defense-in-depth even with
185 auth in place.
186 - Reviewer: scrutinize the token lifecycle (generation, exposure only to the
187 trusted client, comparison), and confirm no privileged endpoint is left ungated.
188
188 lines MARKDOWN