fix(learner): review round 1: sanitize-html, sandboxed runner frame, one visibility rule, no Tab capture
C1: lesson HTML goes through a sanitize-html allowlist on the fully rendered output (no on* attributes, no style, no svg/math/iframe/object/embed/form; URLs limited to http(s), mailto, relative and anchors). Raw tags outside the allowlist are escaped as text. The SPA document is served with a CSP (script-src 'self', no inline script) through spaWithCsp in main.ts; contract request §5 asks core to own it. C2: exercises run in <iframe sandbox="allow-scripts"> (opaque origin, no cookies) loaded from /api/learner/runner-frame, whose CSP allows only its own script (by hash) and blob: workers, and connect-src 'none'. postMessage only, both sides check event.source; the timeout removes the frame. The worker stubs now also cover Worker, SharedWorker and caches. I1: learner visibility calls core canView with per-course authorship only, like /api/media (contract request §6). I2: Tab and Shift+Tab move focus; Enter keeps the previous line's indentation. Tests: XSS unit cases, headless-Chrome checks for the exploit lesson and for fetch, import, Cache.add, nested Worker, img, EventSource, WebSocket, XHR and sendBeacon from learner and test code (plus a negative control), visibility on both dialects, SPA and runner-frame CSP headers. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
061ae7a2d9
commit
c7b9e1f98e
@@ -28,3 +28,17 @@ and let `startServer` take `Array<Feature | FeatureFactory>`.
|
||||
## 4. LessonView for the studio preview
|
||||
|
||||
`import { LessonView } from "@stc/feature-learner/web"`. Props: `lesson: LessonPayload` (the JSON from `GET /api/courses/:id/lessons/:lessonId`), plus optional `look`, `track`, `onTrackChange`, `preview`. With `preview` set, it makes no notes, attempt or progress requests; it still loads code listings and decks through the normal media routes. It injects its own stylesheet (scoped under `.lr`) and works with or without a router.
|
||||
|
||||
## 5. Content-Security-Policy on the SPA, set by core (review C1)
|
||||
|
||||
**Problem.** Lesson HTML is author content. The sanitizer (sanitize-html allowlist) is the first defence, and a CSP without inline script is the second. Bun.serve cannot attach headers to an HTML import route.
|
||||
|
||||
**Workaround in `feat/learner` (applied).** `spaWithCsp(indexHtml, { development })` in `@stc/feature-learner` serves the bundle from an internal server bound to 127.0.0.1 and proxies it. HTML responses get `Content-Security-Policy` (`script-src 'self'`, no `'unsafe-inline'`; `connect-src 'self'`; `object-src 'none'`; `frame-ancestors 'self'`; see `spaCsp()`), plus `X-Content-Type-Options: nosniff` and `Referrer-Policy: same-origin`. `main.ts` uses it for `"/*"`. HMR is off on the internal server, so Bun injects no inline script. If it ever does in development, only the hashes of those exact scripts are allowed.
|
||||
|
||||
**Proposal.** Core owns the SPA route and sets this CSP globally (and for every feature's HTML), replacing the proxy. Other features must then avoid inline scripts as well. The studio preview through `LessonView` already complies.
|
||||
|
||||
## 6. `canView` and the global `author` role (review I1)
|
||||
|
||||
**Ruling applied.** Learner routes now call core `canView` with per-course authorship only (`course_authors`), exactly like core's `/api/media`. A user with only the global `author` role gets a 404 for a restricted course they are not listed on, on every route.
|
||||
|
||||
**Proposal for Phase 2.** Extend core `canView` so the global `author` role can view every course (the studio and AI need the same rule), for example `canView(user, course, acl, isCourseAuthor)` returning true when `user.roles.includes("author")`. The learner then needs no change: it already delegates to `canView`.
|
||||
|
||||
Reference in New Issue
Block a user