Skip to content

feat: Add error reporting feature with email support#2440

Open
MohamedBassem wants to merge 3 commits intomainfrom
claude/add-problem-reporting-gSSEV
Open

feat: Add error reporting feature with email support#2440
MohamedBassem wants to merge 3 commits intomainfrom
claude/add-problem-reporting-gSSEV

Conversation

@MohamedBassem
Copy link
Collaborator

No description provided.

This commit implements a comprehensive problem reporting system:

**Backend:**
- Added ErrorReport plugin type to the plugin system
- Created email-based error reporting plugin that sends reports to a configured support email
- Added SUPPORT_EMAIL configuration option
- Plugin is conditionally registered only if support email is configured
- Added reportProblem tRPC procedure with rate limiting (5 requests per 15 minutes)

**Frontend:**
- Created ReportProblemModal component for users to submit problem reports
- Added "Report a Problem" button to user dropdown menu
- Button only appears when error reporting is enabled via client config
- Added errorReporting.enabled to client configuration

**Configuration:**
- Error reporting is enabled by setting the SUPPORT_EMAIL environment variable
- If no support email is configured, the feature is completely disabled
This commit refactors the error reporting feature to follow best practices:

**Plugin Architecture:**
- Moved SUPPORT_EMAIL config to plugin-specific env.ts (following Restate pattern)
- Added isConfigured() static method to EmailErrorReportProvider
- Plugin self-registers conditionally based on configuration
- Removed conditional loading from shared-server, plugin decides registration

**Client Config:**
- Changed errorReporting.enabled to check PluginManager.isRegistered()
- Updated ProfileOptions to use useClientConfig hook instead of direct import
- Ensures correct plugin state is propagated to frontend

**Router Organization:**
- Moved error reporting from users router to dedicated errorReporting router
- Cleaner separation of concerns
- Updated API path: users.reportProblem → errorReporting.reportProblem

**Debug Information:**
- Added debugInfo parameter to ErrorReportClient interface
- Frontend now captures: userAgent, URL, timestamp
- Backend formats debug info in both HTML and text email templates
- Helps with troubleshooting user-reported issues

**Technical improvements:**
- Removed SUPPORT_EMAIL from main serverConfig (plugin-specific now)
- Added passthrough() to zod schema for extensible debug info
- All changes pass typecheck
…ured

The 'Report a Problem' button now always appears in the user dropdown menu.
- If error reporting plugin is configured: Opens the problem report modal
- If no plugin configured: Opens GitHub issues page in new tab

This ensures users always have a way to report issues, regardless of
server configuration.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 1, 2026

Walkthrough

This pull request introduces an error reporting system that enables users to report problems from the dashboard. It adds a "Report a Problem" modal and menu item to the profile dropdown, integrates a plugin-based backend system for error handlers, and includes an email-based provider using SMTP to send reports with user details and debug information.

Changes

Cohort / File(s) Summary
Dashboard UI Components
apps/web/components/dashboard/header/ProfileOptions.tsx, apps/web/components/dashboard/header/ReportProblemModal.tsx
Adds "Report a Problem" menu item with modal component. ProfileOptions introduces client state to manage modal visibility and conditional routing (to modal or GitHub issues). ReportProblemModal wraps a form with message validation (1-5000 chars), TRPC submission, and debug info collection (userAgent, URL, timestamp).
Client Configuration & Feature Flags
apps/web/lib/clientConfig.tsx, packages/shared/config.ts
Exposes errorReporting feature flag in ClientConfig context. Enables feature based on ErrorReport plugin registration status via PluginManager.
Plugin System Infrastructure
packages/shared/error-reporting.ts, packages/shared/plugins.ts
Introduces ErrorReportClient interface with reportProblem method and PluginProvider generic interface. Extends plugin system with new PluginType.ErrorReport enum and associated type mappings.
Email Error Report Plugin
packages/plugins/errorreport-email/index.ts, packages/plugins/errorreport-email/src/index.ts, packages/plugins/errorreport-email/src/env.ts
Implements email-based error reporting provider. Validates SUPPORT_EMAIL environment variable, creates nodemailer transporter with SMTP config, constructs HTML/text email bodies, and sends reports. Auto-registers plugin on import if configured.
TRPC Router & Backend Integration
packages/trpc/routers/errorReporting.ts, packages/trpc/routers/_app.ts, packages/shared-server/src/plugins.ts
Adds authenticated errorReporting router with rate-limited reportProblem mutation (5 requests per 15 minutes). Retrieves ErrorReport client via PluginManager, validates input, and delegates to configured provider. Registers errorreport-email plugin during server startup.
Package Configuration
packages/plugins/package.json
Adds public export for errorreport-email plugin and nodemailer ^7.0.4 runtime dependency.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No description was provided by the author, making it impossible to assess whether it relates to the changeset. Add a pull request description explaining the error reporting feature, its implementation approach, and how email delivery supports the feature.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately describes the main feature addition: implementing an error reporting capability with email delivery as the primary implementation method.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c54f14ed06

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +455 to +456
errorReporting: {
enabled: PluginManager.isRegistered(PluginType.ErrorReport),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Compute errorReporting.enabled after plugin load

Because clientConfig is a module-level constant, PluginManager.isRegistered(PluginType.ErrorReport) is evaluated during import. In apps/web/app/layout.tsx, clientConfig is imported before await loadAllPlugins(), so the error-report plugin isn’t registered yet and errorReporting.enabled stays false even when SUPPORT_EMAIL is configured. This makes the UI fall back to the GitHub issue flow instead of opening the report modal in environments where email reporting should be active.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link

greptile-apps bot commented Feb 1, 2026

Greptile Overview

Greptile Summary

This PR adds a user-facing “Report a Problem” flow: a new dashboard menu entry opens a modal that submits a message and basic browser debug info to a new authed, rate-limited tRPC endpoint. On the server side, a new ErrorReport plugin type is introduced and an email-based implementation is added using nodemailer, auto-registering when SUPPORT_EMAIL is set; the server also exposes an errorReporting.enabled flag via clientConfig to decide between in-app reporting vs GitHub issues fallback.

Main issues to address are around when errorReporting.enabled is computed (it can be false if config is evaluated before plugin loading) and unescaped interpolation of user-provided fields into HTML emails.

Confidence Score: 3/5

  • This PR is reasonably safe to merge but has a couple of correctness/security-adjacent issues that should be addressed first.
  • The feature is straightforward and well-contained, but (1) the clientConfig flag is derived from PluginManager at module load time and can be wrong depending on bootstrap order, and (2) the email plugin interpolates untrusted user input into HTML emails without escaping. Fixing these reduces the risk of a broken UX and email-client HTML injection.
  • packages/shared/config.ts, packages/plugins/errorreport-email/src/index.ts

Important Files Changed

Filename Overview
apps/web/components/dashboard/header/ProfileOptions.tsx Adds 'Report a Problem' menu item and modal wiring; relies on clientConfig.errorReporting.enabled which may be computed before plugins load.
apps/web/components/dashboard/header/ReportProblemModal.tsx New client modal posting problem reports via tRPC; collects basic debug info and shows toast on success/failure.
apps/web/lib/clientConfig.tsx Extends default ClientConfig context to include errorReporting.enabled flag.
packages/plugins/errorreport-email/index.ts Auto-registers Email error-report provider with PluginManager when SUPPORT_EMAIL is set.
packages/plugins/errorreport-email/src/env.ts Adds env schema parsing for SUPPORT_EMAIL.
packages/plugins/errorreport-email/src/index.ts Implements nodemailer-based ErrorReportClient; currently interpolates user-controlled fields into HTML email without escaping.
packages/plugins/package.json Exports new errorreport-email plugin and adds nodemailer dependency.
packages/shared-server/src/plugins.ts Loads errorreport-email plugin during plugin bootstrap.
packages/shared/config.ts Adds clientConfig.errorReporting.enabled computed from PluginManager.isRegistered, which may be evaluated before plugins are loaded.
packages/shared/error-reporting.ts Introduces ErrorReportClient interface; also defines PluginProvider which duplicates the existing PluginProvider in shared/plugins.ts.
packages/shared/plugins.ts Adds new PluginType.ErrorReport and wiring in PluginManager provider map.
packages/trpc/routers/_app.ts Registers errorReporting router under appRouter.
packages/trpc/routers/errorReporting.ts Adds authed, rate-limited reportProblem endpoint dispatching to ErrorReport plugin client.
pnpm-lock.yaml Locks nodemailer dependency for plugins package.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

14 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +455 to +457
errorReporting: {
enabled: PluginManager.isRegistered(PluginType.ErrorReport),
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P0] clientConfig.errorReporting.enabled is computed from PluginManager.isRegistered(PluginType.ErrorReport) at module-evaluation time, but plugins are loaded later via loadAllPlugins() in packages/shared-server/src/plugins.ts. In environments where @karakeep/shared/config is imported before plugin bootstrap runs, the flag will stay false and the UI will always fall back to GitHub issues even when SUPPORT_EMAIL is configured.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/shared/config.ts
Line: 455:457

Comment:
[P0] `clientConfig.errorReporting.enabled` is computed from `PluginManager.isRegistered(PluginType.ErrorReport)` at module-evaluation time, but plugins are loaded later via `loadAllPlugins()` in `packages/shared-server/src/plugins.ts`. In environments where `@karakeep/shared/config` is imported before plugin bootstrap runs, the flag will stay `false` and the UI will always fall back to GitHub issues even when SUPPORT_EMAIL is configured.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +61 to +63
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2>Problem Report</h2>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] The email html body interpolates user-controlled fields (params.userName, params.userEmail, params.message, and params.debugInfo via JSON.stringify) directly into HTML. If a user includes HTML/JS-looking content, it will be delivered to support inboxes and can cause HTML injection in email clients. Consider HTML-escaping these interpolations (and/or sending plain-text only) to avoid rendering untrusted markup.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/plugins/errorreport-email/src/index.ts
Line: 61:63

Comment:
[P1] The email `html` body interpolates user-controlled fields (`params.userName`, `params.userEmail`, `params.message`, and `params.debugInfo` via `JSON.stringify`) directly into HTML. If a user includes HTML/JS-looking content, it will be delivered to support inboxes and can cause HTML injection in email clients. Consider HTML-escaping these interpolations (and/or sending plain-text only) to avoid rendering untrusted markup.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@apps/web/components/dashboard/header/ProfileOptions.tsx`:
- Around line 62-69: The window.open call in handleReportProblem currently opens
an external link with "_blank" without protection; update the call in the
handleReportProblem function to pass the third parameter "noopener,noreferrer"
(e.g., window.open(url, "_blank", "noopener,noreferrer")) so the opened page
cannot access window.opener and prevent reverse-tabnabbing while preserving the
existing clientConfig.errorReporting and setReportProblemOpen logic.

In `@packages/plugins/errorreport-email/src/index.ts`:
- Around line 92-100: isConfigured() currently only checks
envConfig.SUPPORT_EMAIL but EmailErrorReportClient/reportProblem requires SMTP
settings, so update isConfigured() to also validate the SMTP configuration used
by EmailErrorReportClient (e.g., check envConfig.SMTP or the specific SMTP keys
EmailErrorReportClient expects such as host/port/user/secret) and return false
if those SMTP values are missing or incomplete; this keeps isConfigured()
aligned with EmailErrorReportClient/reportProblem and prevents the UI from
enabling a backend-facing feature that will always fail.
- Around line 11-71: The HTML body in EmailErrorReportClient.reportProblem
interpolates unescaped user-controlled fields (params.userName,
params.userEmail, params.message, and params.debugInfo used in
debugInfoHtml/debugInfoText) into mailOptions.html, allowing HTML/JS injection;
fix by HTML-entity-escaping these fields before interpolation (e.g., create a
small escapeHtml utility and apply it to params.userName, params.userEmail,
params.message and to the JSON string produced for params.debugInfo) and then
use the escaped values in debugInfoHtml, debugInfoText and mailOptions.html to
ensure safe rendering.

In `@packages/plugins/package.json`:
- Line 28: Update the nodemailer dependency in packages/plugins' package.json by
bumping the version string from "nodemailer": "^7.0.4" to "nodemailer":
"^7.0.13" to include fixes for CVE-2025-13033 and CVE-2025-14874; after updating
the dependency entry, run your package manager's install (npm/yarn/pnpm) and
regenerate lockfile to ensure the new version is used in the lockfile and CI.

In `@packages/shared/config.ts`:
- Around line 455-457: The clientConfig currently calls
PluginManager.isRegistered(PluginType.ErrorReport) at module-eval time which
runs before loadAllPlugins(); change clientConfig so the errorReporting.enabled
check is performed at runtime (e.g., make errorReporting.enabled a function or
getter) or expose a factory/getClientConfig() that is called after await
loadAllPlugins(); specifically update the clientConfig export (where
PluginManager.isRegistered is used) to defer evaluation and ensure
apps/web/app/layout.ts imports/uses the deferred check after calling
loadAllPlugins().
🧹 Nitpick comments (1)
packages/shared/error-reporting.ts (1)

16-18: Potential duplicate interface definition.

PluginProvider<T> is defined here and also in packages/shared/plugins.ts (lines 29-31). Consider exporting from a single location to maintain a single source of truth and avoid drift.

♻️ Suggested approach

Either:

  1. Export PluginProvider only from plugins.ts and import it here, or
  2. Export it only from error-reporting.ts and import it in plugins.ts

This prevents the interfaces from diverging over time.

Comment on lines +62 to +69
const handleReportProblem = () => {
if (clientConfig.errorReporting.enabled) {
setReportProblemOpen(true);
} else {
window.open(
"https://github.com/karakeep-app/karakeep/issues/new/choose",
"_blank",
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

window.open _blank noopener noreferrer reverse tabnabbing security modern browsers

💡 Result:

Reverse tabnabbing happens when you open an untrusted page in a new tab/window and it can use window.opener to redirect your original page (e.g., to a phishing site). Blocking access to window.opener is the key defense. [4]

<a target="_blank"> (links)

  • rel="noopener" prevents the new page from getting window.opener. [1]
  • rel="noreferrer" also implies noopener, and additionally suppresses the Referer header (privacy). [2]
  • In modern browsers, target="_blank" on links now implicitly behaves like rel="noopener" (so window.opener is not set). This is in the HTML standard and is noted by MDN/OWASP. [1] [4]

Practical guidance: still consider adding rel="noopener" (or rel="noreferrer" if you also want no referrer) for defense-in-depth and compatibility. [1] [2] [4]

window.open(...) (JavaScript popups/tabs)

For window.open(url, target, features), the HTML standard defines noopener and noreferrer as feature tokens you must pass (they behave like the hyperlink rel values). [3]

Use:

window.open(url, "_blank", "noopener,noreferrer");
  • noopener blocks window.opener access (prevents tabnabbing). [3] [1]
  • noreferrer also blocks the Referer header and implies noopener. [2] [3]

🏁 Script executed:

# Verify the file and code location
cat -n apps/web/components/dashboard/header/ProfileOptions.tsx | sed -n '55,75p'

Repository: karakeep-app/karakeep

Length of output: 777


Add noopener,noreferrer to prevent reverse‑tabnabbing when opening external links.

window.open() with "_blank" requires the third parameter "noopener,noreferrer" to prevent the opened page from accessing window.opener and redirecting the original tab. Unlike <a target="_blank">, this is not implicit in modern browsers and must be explicitly set.

Suggested fix
      window.open(
        "https://github.com/karakeep-app/karakeep/issues/new/choose",
        "_blank",
+       "noopener,noreferrer",
      );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleReportProblem = () => {
if (clientConfig.errorReporting.enabled) {
setReportProblemOpen(true);
} else {
window.open(
"https://github.com/karakeep-app/karakeep/issues/new/choose",
"_blank",
);
const handleReportProblem = () => {
if (clientConfig.errorReporting.enabled) {
setReportProblemOpen(true);
} else {
window.open(
"https://github.com/karakeep-app/karakeep/issues/new/choose",
"_blank",
"noopener,noreferrer",
);
🤖 Prompt for AI Agents
In `@apps/web/components/dashboard/header/ProfileOptions.tsx` around lines 62 -
69, The window.open call in handleReportProblem currently opens an external link
with "_blank" without protection; update the call in the handleReportProblem
function to pass the third parameter "noopener,noreferrer" (e.g.,
window.open(url, "_blank", "noopener,noreferrer")) so the opened page cannot
access window.opener and prevent reverse-tabnabbing while preserving the
existing clientConfig.errorReporting and setReportProblemOpen logic.

Comment on lines +11 to +71
class EmailErrorReportClient implements ErrorReportClient {
async reportProblem(params: {
userId: string;
userName: string;
userEmail: string;
message: string;
debugInfo?: {
userAgent?: string;
url?: string;
timestamp?: string;
[key: string]: unknown;
};
}): Promise<void> {
if (!serverConfig.email.smtp || !envConfig.SUPPORT_EMAIL) {
throw new Error("SMTP or support email is not configured");
}

const transporter = createTransport({
host: serverConfig.email.smtp.host,
port: serverConfig.email.smtp.port,
secure: serverConfig.email.smtp.secure,
auth:
serverConfig.email.smtp.user && serverConfig.email.smtp.password
? {
user: serverConfig.email.smtp.user,
pass: serverConfig.email.smtp.password,
}
: undefined,
});

const debugInfoHtml = params.debugInfo
? `
<hr style="border: 1px solid #eee; margin: 20px 0;">
<h3>Debug Information:</h3>
<pre style="background: #f5f5f5; padding: 10px; border-radius: 5px; overflow-x: auto;">${JSON.stringify(params.debugInfo, null, 2)}</pre>
`
: "";

const debugInfoText = params.debugInfo
? `

Debug Information:
${JSON.stringify(params.debugInfo, null, 2)}
`
: "";

const mailOptions = {
from: serverConfig.email.smtp.from,
to: envConfig.SUPPORT_EMAIL,
subject: `Problem Report from ${params.userName}`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2>Problem Report</h2>
<p><strong>From:</strong> ${params.userName} (${params.userEmail})</p>
<p><strong>User ID:</strong> ${params.userId}</p>
<hr style="border: 1px solid #eee; margin: 20px 0;">
<h3>Message:</h3>
<p style="white-space: pre-wrap;">${params.message}</p>
${debugInfoHtml}
</div>
`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n packages/plugins/errorreport-email/src/index.ts

Repository: karakeep-app/karakeep

Length of output: 3616


🏁 Script executed:

# Search for any existing escape utilities in the codebase
rg -i "escapeHtml|escape|sanitize" packages/plugins/errorreport-email/

Repository: karakeep-app/karakeep

Length of output: 47


🏁 Script executed:

# Check if any HTML escaping libraries are already imported
rg "import.*escape|import.*sanitize" packages/plugins/errorreport-email/

Repository: karakeep-app/karakeep

Length of output: 47


🏁 Script executed:

# Check package.json for any HTML escaping or sanitization libraries
fd "package.json" packages/plugins/errorreport-email/ -exec cat {} \;

Repository: karakeep-app/karakeep

Length of output: 211


🌐 Web query:

Best practices for escaping HTML entities in email content to prevent HTML injection vulnerabilities

💡 Result:

Best practices (HTML email) to prevent HTML injection

  1. Prefer plain text when you don’t need HTML

    • Send text/plain (or multipart/alternative with a plain-text part) so untrusted content never gets interpreted as markup.
  2. Use contextual output encoding (escaping) at render time (recommended default)

    • HTML body/text context (between tags): HTML-entity encode untrusted data so <, >, &, quotes, etc. can’t “break out” into tags/attributes. OWASP specifically recommends HTML entity encoding for HTML contexts. [1]
    • HTML attribute context (inside href="", alt="", etc.): use attribute encoding, keep attributes always quoted, and don’t mix contexts. [1]
    • URL values in attributes: validate + allowlist schemes (typically only http/https), then URL-encode parameter values, then apply attribute encoding. [1]
  3. Do not place untrusted data into “dangerous contexts”

    • Avoid injecting untrusted data into places like <script>, <style>, event-handler attributes (onclick), or other executable contexts—escaping is not reliably safe there. [1]
  4. If you must allow limited user-authored HTML, sanitize with an allowlist HTML sanitizer

    • Output encoding will display markup literally; if you need formatting, use HTML sanitization (allowlist tags/attributes) instead. OWASP recommends DOMPurify for HTML sanitization. [1]
    • If you sanitize, don’t modify the HTML afterward in ways that reintroduce unsafe constructs, and keep the sanitizer patched. [1]
  5. Use well-known encoding/sanitization libraries; don’t hand-roll escaping

    • Example (Java): OWASP Java Encoder provides context-specific encoders like forHtml, forHtmlAttribute, and forUriComponent. [2]
  6. Avoid unsafe HTML string-building APIs in web-rendered email (webmail/admin previews)

    • DOM APIs like innerHTML are injection sinks if fed attacker-controlled strings. Prefer safe insertion methods (e.g., setting text, not HTML). [3]

Sources

[1] OWASP Cross Site Scripting Prevention Cheat Sheet (output encoding rules, dangerous contexts, sanitization, DOMPurify recommendation) — https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
[2] OWASP Java Encoder project (contextual output encoding API examples) — https://owasp.org/www-project-java-encoder/
[3] MDN: Element.innerHTML security warning (HTML parsing/injection sink) — https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML


Escape user-supplied fields before embedding in HTML email.

userName, userEmail, message, and debugInfo are injected directly into the HTML body at lines 64, 68, and 45 without escaping. A crafted input can inject HTML markup or event handlers into the support email. HTML-entity encode these values before embedding.

🔒 Suggested fix (escape before HTML interpolation)
+const escapeHtml = (value: string) =>
+  value
+    .replace(/&/g, "&amp;")
+    .replace(/</g, "&lt;")
+    .replace(/>/g, "&gt;")
+    .replace(/"/g, "&quot;")
+    .replace(/'/g, "&#39;");
+
 class EmailErrorReportClient implements ErrorReportClient {
   async reportProblem(params: {
@@
-    const debugInfoHtml = params.debugInfo
-      ? `
-          <hr style="border: 1px solid `#eee`; margin: 20px 0;">
-          <h3>Debug Information:</h3>
-          <pre style="background: `#f5f5f5`; padding: 10px; border-radius: 5px; overflow-x: auto;">${JSON.stringify(params.debugInfo, null, 2)}</pre>
-        `
-      : "";
+    const debugInfoJson = params.debugInfo
+      ? JSON.stringify(params.debugInfo, null, 2)
+      : "";
+    const debugInfoHtml = debugInfoJson
+      ? `
+          <hr style="border: 1px solid `#eee`; margin: 20px 0;">
+          <h3>Debug Information:</h3>
+          <pre style="background: `#f5f5f5`; padding: 10px; border-radius: 5px; overflow-x: auto;">${escapeHtml(debugInfoJson)}</pre>
+        `
+      : "";
@@
           subject: `Problem Report from ${params.userName}`,
           html: `
             <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
               <h2>Problem Report</h2>
-              <p><strong>From:</strong> ${params.userName} (${params.userEmail})</p>
+              <p><strong>From:</strong> ${escapeHtml(params.userName)} (${escapeHtml(params.userEmail)})</p>
               <p><strong>User ID:</strong> ${params.userId}</p>
               <hr style="border: 1px solid `#eee`; margin: 20px 0;">
               <h3>Message:</h3>
-              <p style="white-space: pre-wrap;">${params.message}</p>
+              <p style="white-space: pre-wrap;">${escapeHtml(params.message)}</p>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class EmailErrorReportClient implements ErrorReportClient {
async reportProblem(params: {
userId: string;
userName: string;
userEmail: string;
message: string;
debugInfo?: {
userAgent?: string;
url?: string;
timestamp?: string;
[key: string]: unknown;
};
}): Promise<void> {
if (!serverConfig.email.smtp || !envConfig.SUPPORT_EMAIL) {
throw new Error("SMTP or support email is not configured");
}
const transporter = createTransport({
host: serverConfig.email.smtp.host,
port: serverConfig.email.smtp.port,
secure: serverConfig.email.smtp.secure,
auth:
serverConfig.email.smtp.user && serverConfig.email.smtp.password
? {
user: serverConfig.email.smtp.user,
pass: serverConfig.email.smtp.password,
}
: undefined,
});
const debugInfoHtml = params.debugInfo
? `
<hr style="border: 1px solid #eee; margin: 20px 0;">
<h3>Debug Information:</h3>
<pre style="background: #f5f5f5; padding: 10px; border-radius: 5px; overflow-x: auto;">${JSON.stringify(params.debugInfo, null, 2)}</pre>
`
: "";
const debugInfoText = params.debugInfo
? `
Debug Information:
${JSON.stringify(params.debugInfo, null, 2)}
`
: "";
const mailOptions = {
from: serverConfig.email.smtp.from,
to: envConfig.SUPPORT_EMAIL,
subject: `Problem Report from ${params.userName}`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2>Problem Report</h2>
<p><strong>From:</strong> ${params.userName} (${params.userEmail})</p>
<p><strong>User ID:</strong> ${params.userId}</p>
<hr style="border: 1px solid #eee; margin: 20px 0;">
<h3>Message:</h3>
<p style="white-space: pre-wrap;">${params.message}</p>
${debugInfoHtml}
</div>
`,
const escapeHtml = (value: string) =>
value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
class EmailErrorReportClient implements ErrorReportClient {
async reportProblem(params: {
userId: string;
userName: string;
userEmail: string;
message: string;
debugInfo?: {
userAgent?: string;
url?: string;
timestamp?: string;
[key: string]: unknown;
};
}): Promise<void> {
if (!serverConfig.email.smtp || !envConfig.SUPPORT_EMAIL) {
throw new Error("SMTP or support email is not configured");
}
const transporter = createTransport({
host: serverConfig.email.smtp.host,
port: serverConfig.email.smtp.port,
secure: serverConfig.email.smtp.secure,
auth:
serverConfig.email.smtp.user && serverConfig.email.smtp.password
? {
user: serverConfig.email.smtp.user,
pass: serverConfig.email.smtp.password,
}
: undefined,
});
const debugInfoJson = params.debugInfo
? JSON.stringify(params.debugInfo, null, 2)
: "";
const debugInfoHtml = debugInfoJson
? `
<hr style="border: 1px solid `#eee`; margin: 20px 0;">
<h3>Debug Information:</h3>
<pre style="background: `#f5f5f5`; padding: 10px; border-radius: 5px; overflow-x: auto;">${escapeHtml(debugInfoJson)}</pre>
`
: "";
const debugInfoText = params.debugInfo
? `
Debug Information:
${JSON.stringify(params.debugInfo, null, 2)}
`
: "";
const mailOptions = {
from: serverConfig.email.smtp.from,
to: envConfig.SUPPORT_EMAIL,
subject: `Problem Report from ${params.userName}`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2>Problem Report</h2>
<p><strong>From:</strong> ${escapeHtml(params.userName)} (${escapeHtml(params.userEmail)})</p>
<p><strong>User ID:</strong> ${params.userId}</p>
<hr style="border: 1px solid `#eee`; margin: 20px 0;">
<h3>Message:</h3>
<p style="white-space: pre-wrap;">${escapeHtml(params.message)}</p>
${debugInfoHtml}
</div>
`,
🤖 Prompt for AI Agents
In `@packages/plugins/errorreport-email/src/index.ts` around lines 11 - 71, The
HTML body in EmailErrorReportClient.reportProblem interpolates unescaped
user-controlled fields (params.userName, params.userEmail, params.message, and
params.debugInfo used in debugInfoHtml/debugInfoText) into mailOptions.html,
allowing HTML/JS injection; fix by HTML-entity-escaping these fields before
interpolation (e.g., create a small escapeHtml utility and apply it to
params.userName, params.userEmail, params.message and to the JSON string
produced for params.debugInfo) and then use the escaped values in debugInfoHtml,
debugInfoText and mailOptions.html to ensure safe rendering.

Comment on lines +92 to +100
static isConfigured(): boolean {
return envConfig.SUPPORT_EMAIL !== undefined;
}

async getClient(): Promise<ErrorReportClient | null> {
if (!this.client) {
this.client = new EmailErrorReportClient();
}
return this.client;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Align isConfigured with SMTP requirements to avoid runtime failure.

isConfigured() only checks SUPPORT_EMAIL, but reportProblem throws if SMTP is missing. This can surface the UI while the backend will always fail. Consider validating the SMTP config here as well.

✅ Suggested fix
 static isConfigured(): boolean {
-  return envConfig.SUPPORT_EMAIL !== undefined;
+  const smtp = serverConfig.email.smtp;
+  return (
+    !!envConfig.SUPPORT_EMAIL &&
+    !!smtp &&
+    !!smtp.host &&
+    !!smtp.port &&
+    !!smtp.from
+  );
 }
🤖 Prompt for AI Agents
In `@packages/plugins/errorreport-email/src/index.ts` around lines 92 - 100,
isConfigured() currently only checks envConfig.SUPPORT_EMAIL but
EmailErrorReportClient/reportProblem requires SMTP settings, so update
isConfigured() to also validate the SMTP configuration used by
EmailErrorReportClient (e.g., check envConfig.SMTP or the specific SMTP keys
EmailErrorReportClient expects such as host/port/user/secret) and return false
if those SMTP values are missing or incomplete; this keeps isConfigured()
aligned with EmailErrorReportClient/reportProblem and prevents the UI from
enabling a backend-facing feature that will always fail.

"liteque": "^0.7.0",
"meilisearch": "^0.45.0"
"meilisearch": "^0.45.0",
"nodemailer": "^7.0.4"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Get latest nodemailer version and recent versions
npm view nodemailer versions --json 2>/dev/null | tail -20

echo "---"

# Get specific info about nodemailer 7.0.4
npm view nodemailer@7.0.4 2>/dev/null | head -30

echo "---"

# Check for advisories using npm audit with specific package
npm audit --json 2>/dev/null | jq '.vulnerabilities' 2>/dev/null || echo "No audit info"

Repository: karakeep-app/karakeep

Length of output: 857


🌐 Web query:

nodemailer 7.0.4 security vulnerabilities CVE

💡 Result:

nodemailer@7.0.4 is affected by these known CVEs:

  • CVE-2025-13033 — email recipient misrouting / “interpretation conflict” in address parsing (can redirect mail to an unintended domain). Affected: < 7.0.7 → so 7.0.4 is vulnerable. Fix: upgrade to 7.0.7+. Sources: NVD [1], GitHub advisory (GHSA-mm7p-fcc7-pg87) [2].
  • CVE-2025-14874DoS via uncontrolled/infinite recursion in the address parser from crafted address headers. Affected: < 7.0.11 → so 7.0.4 is vulnerable. Fix: upgrade to 7.0.11+. Sources: NVD [3], GitLab advisory database [4].

Recommended upgrade target: 7.0.11+ (covers both). [2], [4]


🌐 Web query:

nodemailer latest version 2024 2025

💡 Result:

  • Latest Nodemailer release in 2024: 6.9.16 (released Oct 28, 2024) [1]
  • Latest Nodemailer release in 2025: 7.0.12 (released Dec 22, 2025) [2]
  • Note: 6.10.0 was released Jan 23, 2025 (so it’s 2025, not 2024) [1][3]
  • If you meant “latest overall” (now): 7.0.13 (released Jan 27, 2026) [2]

Sources: [1] [2] [3]


Update nodemailer to 7.0.13 or higher to address critical security vulnerabilities.

Version 7.0.4 is affected by:

  • CVE-2025-13033: Email recipient misrouting via address parsing (fixed in 7.0.7+)
  • CVE-2025-14874: DoS via infinite recursion in address parser (fixed in 7.0.11+)

Latest stable version 7.0.13 addresses both. Upgrade to "nodemailer": "^7.0.13".

🤖 Prompt for AI Agents
In `@packages/plugins/package.json` at line 28, Update the nodemailer dependency
in packages/plugins' package.json by bumping the version string from
"nodemailer": "^7.0.4" to "nodemailer": "^7.0.13" to include fixes for
CVE-2025-13033 and CVE-2025-14874; after updating the dependency entry, run your
package manager's install (npm/yarn/pnpm) and regenerate lockfile to ensure the
new version is used in the lockfile and CI.

Comment on lines +455 to +457
errorReporting: {
enabled: PluginManager.isRegistered(PluginType.ErrorReport),
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check where clientConfig is imported and ensure loadAllPlugins is called first
echo "=== Files importing clientConfig ==="
rg -l "from ['\"]@karakeep/shared/config['\"]|from ['\"].*config['\"]" --type ts | head -20

echo ""
echo "=== Check server entry points for plugin loading order ==="
# Look for loadAllPlugins calls
rg -n "loadAllPlugins" --type ts -C 3

Repository: karakeep-app/karakeep

Length of output: 4211


🏁 Script executed:

# Let's also check the structure of the config.ts file to understand the context better
echo "=== Content of packages/shared/config.ts around lines 455-457 ==="
sed -n '450,465p' packages/shared/config.ts

echo ""
echo "=== Check imports at top of config.ts ==="
head -20 packages/shared/config.ts

Repository: karakeep-app/karakeep

Length of output: 1076


🏁 Script executed:

# Find the plugins.ts file and understand loadAllPlugins
echo "=== Find plugins.ts and check loadAllPlugins ==="
find . -name "plugins.ts" -type f | head -5

echo ""
echo "=== Examine loadAllPlugins function ==="
rg -A 10 "export.*loadAllPlugins" --type ts

Repository: karakeep-app/karakeep

Length of output: 1327


🏁 Script executed:

# Check the full context around the problematic import/await in layout.tsx
echo "=== apps/web/app/layout.tsx lines 1-30 ==="
sed -n '1,30p' apps/web/app/layout.tsx

echo ""
echo "=== Verify packages/shared/config.ts exports clientConfig ==="
rg -n "^export.*clientConfig|^const clientConfig" packages/shared/config.ts

Repository: karakeep-app/karakeep

Length of output: 1234


🏁 Script executed:

# Check if there's a separate serverConfig that should be used instead
echo "=== Check what clientConfig and serverConfig are ==="
rg -B2 "^const (client|server)Config\s*=" packages/shared/config.ts | head -20

Repository: karakeep-app/karakeep

Length of output: 117


Fix plugin initialization order in the web app.

PluginManager.isRegistered(PluginType.ErrorReport) is called at module load time when clientConfig is constructed. In apps/web/app/layout.tsx, the import of clientConfig (line 17) occurs before loadAllPlugins() is awaited (line 19), causing the ErrorReport plugin to not be registered when the check runs. Move the clientConfig import to after the await loadAllPlugins() call, or ensure plugins are loaded before this module evaluates.

Other entry points (apps/workers/index.ts, packages/api/index.ts) have the correct initialization order and are not affected.

🤖 Prompt for AI Agents
In `@packages/shared/config.ts` around lines 455 - 457, The clientConfig currently
calls PluginManager.isRegistered(PluginType.ErrorReport) at module-eval time
which runs before loadAllPlugins(); change clientConfig so the
errorReporting.enabled check is performed at runtime (e.g., make
errorReporting.enabled a function or getter) or expose a
factory/getClientConfig() that is called after await loadAllPlugins();
specifically update the clientConfig export (where PluginManager.isRegistered is
used) to defer evaluation and ensure apps/web/app/layout.ts imports/uses the
deferred check after calling loadAllPlugins().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants