§9 whole-project arch+product review pass (todo §9); ran systems-architect + product-owner on the whole project (no Critical/High — a converged scaffold) and addressed the in-scope §9 customer-facing/security findings. (1) return_to deep-link login, open-redirect-safe: a gated request hit while signed out (plugin-route gate, requireSession, requireAdmin) bounces to /login?return_to=<host-relative path> via new loginRedirect(ctx) (GET/HEAD, skips /); /login bakes it into the Kratos flow — a host-relative target is wrapped through <origin>/auth/complete?return_to=<path> so the JWT mints before landing, an absolute target (§6 OAuth2 login challenge) passes to Kratos as-is; /auth/complete redirects to the requested page. (2) safeUrl()+localPath() in new pure src/safe-url.ts: safeUrl sanitises an untrusted href/src to relative-or-http(s) (else "#"), exported via plugin-api.ts (closes the contract's "planned for §9" pointer); localPath is the host-relative redirect-allowlist guard for return_to, re-checked at both /login and /auth/complete. (3) honest 503 on Ory-unreachable sign-in (views/503.ejs) instead of the misattributed catch-all 500; expired-flow 4xx still restarts. Tests-first throughout; stability-reviewer APPROVE (addressed its Medium — scoped the 503 catch so a template bug hits the 500 with a stack, not a 503). typecheck + 339 units + full scripts/ci.sh gate green. Deferred with justification: the app.ts route-table refactor (standalone change + §10 prereq), mock dashboard + public-page blessing (§10 lines 139/140), success-flash (known).
This commit is contained in:
@@ -112,9 +112,9 @@ path `/<id>`** (so `/shifts` in the `scheduling` plugin serves `/scheduling/shif
|
||||
matches `method` + the resolved full path, extracts `:name` segments into `ctx.params.name`,
|
||||
runs the `permission` gate (a coarse JWT-claim check — see the README), and only then calls the
|
||||
handler with the [request context](#requestcontext). When the gate fails, an **anonymous** visitor
|
||||
is redirected to `/login` to sign in (same as the built-in admin screens; after login they land on
|
||||
the dashboard, not back on the requested page); a **signed-in** user who simply lacks the role gets
|
||||
the **403** page.
|
||||
is redirected to `/login` to sign in (same as the built-in admin screens); the requested page is
|
||||
preserved as `return_to`, so after signing in they land **back on the page they asked for**, not the
|
||||
dashboard. A **signed-in** user who simply lacks the role gets the **403** page.
|
||||
|
||||
`method` is one of `GET HEAD POST PUT PATCH DELETE`. A `GET` route also answers `HEAD`.
|
||||
|
||||
@@ -168,8 +168,12 @@ safety of the data it renders**:
|
||||
names), so those are injection-safe. But a URL field — nav `href`, a table cell link, a menu
|
||||
item, a breadcrumb, `brand.logo` — is emitted as-is inside the attribute: a `javascript:` or
|
||||
`data:` URL from upstream/user data becomes live XSS. When a URL comes from data you don't
|
||||
control, restrict it to a relative (`/`, `?`, `#`) or `http(s):` URL before handing it to a
|
||||
partial. (A shared `safeUrl()` helper is planned for §9, with the redirect-URI allowlist work.)
|
||||
control, pass it through **`safeUrl()`** from `src/plugin-api.ts` first — it returns the URL when
|
||||
it's relative or `http(s):` and collapses anything else to `"#"`:
|
||||
```ts
|
||||
import { safeUrl } from "../../src/plugin-api.ts";
|
||||
return { view: "list", data: { rows: rows.map((r) => ({ ...r, href: safeUrl(r.href) })) } };
|
||||
```
|
||||
|
||||
## RequestContext
|
||||
|
||||
|
||||
Reference in New Issue
Block a user