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:
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user