Two problems with the upload scanner, pulling in opposite directions.
It only scanned the entrypoint, so a bundle with `eval()` in a second
file passed outright — verified against a synthetic bundle whose
vendor/openpgp.js tripped three patterns while index.js stayed clean.
At the same time, a hard 400 on eval()/new Function()/innerHTML= makes
every crypto plugin uninstallable: minified openpgp.js and pkijs
legitimately contain all three. That blocks S/MIME and PGP entirely.
Scan every .js/.mjs in the bundle and return structured findings
({file, patterns[]}) plus canOverride, so the admin can see exactly what
tripped and where. An explicit overrideWarnings=true proceeds and writes
a plugin.install.scan_override audit entry recording which patterns in
which files were accepted — not merely that an override happened.
This route is already admin-authenticated, so the scan is defence in
depth against an accidental or compromised upload, not a trust boundary.
Treating it as the latter is what made crypto plugins uninstallable.
Also log the B-04 and B-01 divergences in vnc/VNC-CHANGES.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.1 KiB
VNC-CHANGES — VNCmail+ divergence log
VNCmail+ is a fork of bulwarkmail/webmail (AGPL-3.0). This file records every intentional divergence from upstream so that merging new upstream releases stays a triage exercise, not an archaeology dig.
Rules of the fork
- Keep upstream files unmodified whenever possible. Prefer env vars
(branding), the
vnc/overrides/layer, and additive files over editing files insideapp/,components/,lib/,stores/. - Every edit to an upstream file gets a one-line entry below — path, what, why. No silent edits.
- Branches:
maintracks upstream releases (kept clean);devis the VNC integration + Vercel deploy branch;vnc/*are feature branches. - Syncing upstream:
git fetch upstream && git merge upstream/mainontomain, then mergemainintodev. Resolve using this log.
Divergences
| Date | File / area | Change | Why |
|---|---|---|---|
| 2026-08-03 | vnc/ (new) |
Added VNC customization dir + this log | Fork bootstrap |
| 2026-08-03 | VNCMAIL-SETUP.md (new) |
Vercel deploy runbook | Deploy on Vercel as project "VNCmail+" |
| 2026-08-03 | deploy/k8s/ (new) | k8s manifests + runbook for microk8s deploy | Bulwark is stateful → runs as a container w/ persistent volumes, not Vercel serverless |
| 2026-08-03 | .gitignore | ignore deploy/k8s/secret.yaml | keep the real env secret out of git |
| 2026-08-03 | public/branding/*.svg (new) | VNCmail wordmark (on-dark + on-light) | VNC logo (placeholder — swap official SVG) |
| 2026-08-03 | public/fonts/*.woff2 (new) | DM Sans 400/500/700 + Syne 700/800 (OFL, self-hosted) | VNClagoon typography |
| 2026-08-03 | lib/builtin-themes.ts | add builtin-vnclagoon theme (navy+cyan, DM Sans/Syne, @font-face) | VNClagoon brand theme |
| 2026-08-03 | lib/admin/types.ts | DEFAULT_THEME_POLICY.defaultThemeId → builtin-vnclagoon | make VNClagoon the default theme |
| 2026-08-03 | stores/theme-store.ts | default theme/resolvedTheme → dark | dark-first per VNClagoon styleguide |
| 2026-08-03 | lib/builtin-themes.ts | add builtin-src theme (Swiss red on white, light-first) | 2nd brand theme (SRC Advisory); VNClagoon stays default |
| 2026-08-03 | public/branding/src-logo.svg (new) | SRC mountain mark | SRC brand (placeholder — swap official) |
| 2026-08-03 | lib/plugin-types.ts | add optional logoLightUrl/logoDarkUrl to InstalledTheme | per-theme brand logos |
| 2026-08-03 | lib/theme-logo.ts (new) | resolveThemeLogo() helper | pick active theme's logo, fall back to global |
| 2026-08-03 | lib/builtin-themes.ts | set logos on vnclagoon (wordmark) + src (mark) | logo switches with the brand theme |
| 2026-08-03 | app/(main)/[locale]/login/page.tsx | login logo uses active theme's logo | brand-switch on login |
| 2026-08-03 | components/layout/navigation-rail.tsx | nav-rail logo uses active theme's logo | brand-switch in app |
Note: Vercel was tried and abandoned on 2026-08-03. Bulwark writes to a local
data dir (/app/data/*); Vercel serverless has a read-only filesystem → crash
(ENOENT /var/task/data). VNCmail+ now deploys as a Docker image
(ghcr.io/brvncde-dotcom/vncmail-plus-*) on Kubernetes (microk8s) at
vncmail.sandbox.vnc.de, with 4 persistent volumes — see deploy/k8s/. A
microfrontends integration was also added and reverted the same day.
| 2026-08-03 | lib/stalwart/auth-context.ts | give jmap_stalwart_ctx a 6-hour maxAge (was session-cookie → expired on tab close) | session survival across browser restarts |
| 2026-08-03 | lib/builtin-themes.ts | add srcSkin (MD3 component overrides: shape scale, filled buttons, text fields, cards, dialogs, state layers, switches, login card); add @font-face + typography to builtin-src; bump to v1.1.0 | SRC theme: keep colors + fonts, apply MD3 design system |
| 2026-08-04 | lib/plugin-sandbox/loader.ts | B-04 security fix — gate hook registration on granted permissions via new HOOK_PERMISSIONS map; refused hooks are skipped, logged and counted | info.hooks is self-reported by the sandbox, so an untrusted plugin could claim onRenderEmailBody and replace any rendered email body without holding email:render-takeover. Consent copy gated what the user was asked, not what the host allowed. |
| 2026-08-04 | lib/plugin-sandbox/host-api.ts | export hasPermission() (was module-private) | one source of truth for the permission rule — the loader gate and the RPC gate must not drift apart |
| 2026-08-04 | app/api/admin/plugins/route.ts | B-01 — scan all .js/.mjs in the bundle (was entrypoint only); return structured findings + canOverride; allow admin overrideWarnings=true with a plugin.install.scan_override audit entry | hard-reject on eval(/new Function(/innerHTML = made every crypto plugin uninstallable (minified openpgp.js/pkijs trip it), while only scanning the entrypoint left a trivial bypass. Route is already admin-authenticated, so the scan is defence-in-depth, not a trust boundary. |
(append new rows as you diverge)