fix(audit): H11 — gate cross-port coverBrandPortId in report runs

Layer 1: createReportRun rejects a user-triggered run whose coverBrandPortId
is a port the triggering user can't access (userCanAccessPort: super-admin or
userPortRoles membership). Layer 2: renderReportRun only honors the override
when it equals run.portId or the run's user is a member, else falls back to
the source port's branding — so a forged/scheduled config can't leak another
tenant's logo/name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 12:18:11 +02:00
parent a335dbc117
commit 1882bcb2e4
3 changed files with 88 additions and 8 deletions

View File

@@ -72,7 +72,12 @@ export const createReportRunSchema = z.object({
kind: z.enum(REPORT_KINDS),
templateId: z.string().optional(),
// Same opaque shape report_templates accepts — the render queue
// re-validates per-kind at use time.
// re-validates per-kind at use time. NOTE: the optional
// `config.coverBrandPortId` (cover-page brand swap) is intentionally NOT
// shape-validated here — it requires a runtime per-user membership check
// that Zod can't express. `createReportRun` rejects an override the
// triggering user can't access (H11), and the renderer strips a
// stale/forged one as defense-in-depth.
config: z.record(z.string(), z.unknown()),
outputFormat: z.enum(REPORT_OUTPUT_FORMATS).default('pdf'),
emailTo: z.array(recipientSchema).max(50).optional(),