Repository navigation
Apply tenant brand colors to all transactional emails - #88
Conversation
|
Semgrep found 1 🟠 High severity issue identified in your code: Risk: Affected versions of nodemailer are vulnerable to Inefficient Algorithmic Complexity / Inefficient Regular Expression Complexity. When strict parsing of an address token fails, Nodemailer's addressparser falls back to extracting an address from free text with the unanchored regex /\s*\b[^@\s]+@[^\s]+\b\s*/, which backtracks quadratically on long whitespace-free runs that contain no usable '@'. An attacker who can get such a string into any value that reaches the parser — a From/To/Cc/Bcc/Sender/Reply-To value passed to sendMail, an address header on a MimeNode that is later built or turned into an envelope, or a string handed directly to the exported addressparser function — can block the Node.js event loop for tens of seconds (about 273 KB causes roughly 43 seconds of blocking), denying service to the whole application. Fix: Upgrade this library to at least version 10.0.6 at openpartner/pnpm-lock.yaml:2361. Reference(s): GHSA-v53p-9fqp-m79j View Dataflow Graphflowchart LR
classDef invis fill:white, stroke: none
classDef default fill:#e7f5ff, color:#1c7fd6, stroke: none
subgraph File0["<b>apps/api/src/mailer.ts</b>"]
direction LR
%% Source
subgraph Source
direction LR
v0["<a href=https://github.com/getcoherence/openpartner/blob/44065cbe5efff444064c469b1be8f6c9cd44835a/apps/api/src/mailer.ts#L119 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 119] nodemailer.createTransport({<br> host: cfg.smtp.host,<br> port: cfg.smtp.port,<br> secure: cfg.smtp.secure,<br> auth:<br> cfg.smtp.user && cfg.smtp.password<br> ? { user: cfg.smtp.user, pass: cfg.smtp.password }<br> : undefined,<br> // Nodemailer's default connection timeout is 2 MINUTES — longer<br> // than the platform gateway's ~60s, so a misconfigured host (wrong<br> // hostname, filtered port) surfaced as an opaque 504 instead of<br> // our clean ETIMEDOUT/ECONNREFUSED error, and a real send (partner<br> // invite, signup) could pin its request just as long. Fail fast:<br> // a healthy SMTP handshake completes in single-digit seconds.<br> connectionTimeout: 10_000,<br> greetingTimeout: 10_000,<br> socketTimeout: 30_000,<br> })</a>"]
end
%% Intermediate
subgraph Traces0[Traces]
direction TB
v2["<a href=https://github.com/getcoherence/openpartner/blob/44065cbe5efff444064c469b1be8f6c9cd44835a/apps/api/src/mailer.ts#L119 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 119] transporter</a>"]
end
%% Sink
subgraph Sink
direction LR
v1["<a href=https://github.com/getcoherence/openpartner/blob/44065cbe5efff444064c469b1be8f6c9cd44835a/apps/api/src/mailer.ts#L137 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 137] transporter.sendMail({<br> from,<br> to: msg.to,<br> replyTo,<br> subject: msg.subject,<br> text: msg.text,<br> html,<br> headers: msg.tag ? { 'X-Tag': msg.tag } : undefined,<br> })</a>"]
end
end
%% Class Assignment
Source:::invis
Sink:::invis
Traces0:::invis
File0:::invis
%% Connections
Source --> Traces0
Traces0 --> Sink
Semgrep found 1 🟠 High severity issue identified in your code: Risk: Affected versions of nodemailer are vulnerable to Inefficient Algorithmic Complexity / Uncontrolled Resource Consumption. Nodemailer's RFC 5322 address parser accumulates parsed addresses with Array.prototype.concat(), splices display-name fragments out of the accumulator, and deduplicates recipients with a linear scan per address, all of which run in quadratic O(n^2) time. By supplying a large comma-separated address list to any field that reaches the parser — a recipient/Reply-To/From value passed to sendMail, an address header set on a MimeNode and then encoded when the message is built, or a string handed directly to the exported addressparser function — an attacker can pin the CPU at 100% and block the Node.js event loop for tens of seconds (a ~1.5 MB list stalls the process for 25-30 seconds), denying service to the whole application. Fix: Upgrade this library to at least version 9.1.0 at openpartner/pnpm-lock.yaml:2361. Reference(s): GHSA-2x7j-588g-ccc2 |
|
Thanks for flagging these findings. Both advisories affect the existing upstream dependency, Upgrading Nodemailer to at least 10.0.6 addresses both reported address-parser denial-of-service issues. Existing security PR #85 includes Nodemailer remediation, and Dependabot PR #82 proposes 10.0.9. I recommend merging the dependency remediation separately, then updating this branding branch against the patched main branch and rerunning the checks. The existing audit failure is also disclosed in this PR's validation notes. |
The workspace sign-in (/signin) runs under whichever Admin row matched first, so it was painted in an arbitrary brand's color; ops alerts and the ops-console sign-in went out in the default tenant's color. A Message can now be marked `platform`, which skips tenant branding, and those three sends set it. The marker rewrite is also anchored to our own <table>/<a> tags, so text that merely contains `data-op-email=` (an escaped partner name) is left alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
keithfawcett
left a comment
There was a problem hiding this comment.
Thanks! Pushed a small follow-up: platform-voice emails (sign-in, ops alerts) are never brand-colored, and the marker rewrite is anchored to our own tags.
What
Apply the tenant's configured
brandColorto all OpenPartner transactional emails, including admin/partner magic links, invitations, campaign notices, account suspension, brand-review and platform-ops/billing notifications. Previously the email templates always used the platform teal even when the portal used a custom brand color.Why
The existing brand setting should carry through to email. This also gives messages without a CTA and the mail-transport test a branded HTML card; the plain-text alternative remains intact.
How
#RGB,#RRGGBB,#RRGGBBAA); strip alpha like the portal and fall back to teal for missing/invalid values. Choose black/white button text and darken light link colors to maintain at least 4.5:1 contrast on white.How to verify
Use Node 22, pnpm 9 and a disposable PostgreSQL 16 database, then run:
pnpm install --frozen-lockfile pnpm --filter @openpartner/db build pnpm migrate pnpm lint pnpm typecheck pnpm test pnpm --filter @openpartner/api buildThe new Vitest file covers every template, safe fallback, shorthand/alpha colors, contrast, unchanged custom HTML and fresh tenant-scoped branding through mocked SMTP/Postmark transports. Both transport regressions fail with the original mailer because the delivered HTML still contains the fixed teal button.
Local validation used Node 22.20.0 / pnpm 9.15.4 / PostgreSQL 16.15 and the upstream frozen lockfile:
git diff --check.funding-races.test.tssweep-limit assertions fail (empty skipped list / five checked charges instead of two). The focused funding suite and final complete rerun passed without funding-code changes; root cause remains unconfirmed.baseUrldeprecation under TypeScript 6); the affected API build passes independently.No live email or deployment was performed; Gmail/Outlook rendering remains unverified.
Checklist
pnpm typecheck+pnpm testgreen locally (final complete rerun)