CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Dashboard
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-27116
6.1

Vikunja HTML Injection: When a 'Filter' Becomes a Phishing Hook

Alon Barad
Alon Barad
Software Engineer

Feb 26, 2026·7 min read·5 visits

PoC Available

Executive Summary (TL;DR)

Vikunja < 2.0.0 improperly handles the `filter` URL parameter when initializing the TipTap text editor. By passing a raw string to the editor's `setContent` method, the application inadvertently triggers HTML parsing. This allows attackers to inject specific HTML tags (like `<a>`, `<svg>`, `<h1>`), bypassing some sanitization, to conduct high-confidence phishing attacks or content spoofing.

A Reflected HTML Injection vulnerability in the Vikunja task management platform allows attackers to inject arbitrary HTML tags into the application's DOM via the filter parameter. By exploiting the way the TipTap editor parses string content, attackers can craft convincing phishing scenarios or redirect users to malicious sites, all under the guise of a trusted application URL.

The Hook: Organizing Tasks and Phishing Campaigns

Vikunja is the darling of the self-hosted productivity world. It’s slick, it’s written in Go, and it promises to organize your life without selling your soul to a SaaS giant. But in versions prior to 2.0.0, it also offered a feature nobody asked for: a built-in platform for hosting high-fidelity phishing campaigns directly inside your own dashboard.

The vulnerability lies in a classic disconnect between modern frontend frameworks and the underlying libraries they abstract. We often assume that using a framework like Vue.js protects us from the sins of the past—like Reflected XSS or HTML Injection. Usually, it does. But when you start integrating rich text editors, you enter the Wild West of DOM manipulation. In this case, the Projects module allowed users to filter tasks via a URL parameter. Convenient? Yes. Safe? Absolutely not.

What makes CVE-2026-27116 interesting isn't that it's a complex buffer overflow or a heap grooming masterpiece. It's a logic error in how data is handed off to TipTap, a wrapper around the ProseMirror editor. The developer treated user input like a simple string, but the library treated it like a browser innerHTML assignment. This mismatch turns a simple 'search filter' into a canvas for attackers to paint whatever reality they want your users to see.

The Flaw: The 'Magic' of setContent()

To understand this bug, you have to understand how TipTap handles content. It’s a powerful editor that tries to be helpful. Too helpful. The core method involved here is editor.commands.setContent(). This function is a bit of a chameleon. If you pass it a structured JSON object (a ProseMirror document node), it renders exactly what you tell it to. It treats text as text, bold as bold, and so on. It is type-strict and safe.

However, if you pass setContent() a raw string, TipTap assumes you are lazy and tries to do the heavy lifting for you. It invokes a DOMParser to parse that string as HTML. It’s the library equivalent of saying, "Oh, you gave me a blob of text? Let me just run this through the browser's HTML engine and see what happens." This is the root cause: Implicit Type Coercion leads to Implicit HTML Parsing.

In frontend/src/components/input/filter/FilterInput.vue, the code was taking the filter query parameter directly from the URL—unverified, unloved, and unclean—and shoving it straight into setContent(). The developer likely intended for the filter to just populate the text box so the user could see what they were searching for. instead, they built a mechanism that takes ?filter=<h1>Surprise</h1> and actually renders a header tag in the UI. It's like trying to display a license plate number but accidentally letting the driver rewrite the traffic laws.

The Code: From String to Schema

Let's look at the smoking gun. This is the code inside FilterInput.vue responsible for initializing the editor with the filter value. In the vulnerable version, the input is treated as a raw string.

Vulnerable Code:

function setEditorContentFromModelValue(newValue: string | undefined) {
    // ... checks ...
    // 'content' here comes directly from the URL query parameter
    editor.value.commands.setContent(content, {
        emitUpdate: false,
    })
}

When content is a string containing HTML tags, TipTap parses it. The fix involves forcing TipTap to treat the input as a specific node type—specifically, a paragraph of text. By constructing a ProseMirror JSON object manually, the developer removes ambiguity. The editor no longer has to guess if the input is HTML; it is explicitly told, "This is a text node. Render it literally."

The Fix (Commit a42b4f37bde58596a3b69482cd5a67641a94f62d):

// Use JSON content format instead of a plain string to prevent
// TipTap from parsing the value as HTML
editor.value.commands.setContent(content
    ? {
        type: 'doc',
        content: [{
            type: 'paragraph',
            content: [{type: 'text', text: content}],
        }],
    }
    : '', {emitUpdate: false})

Notice the structure. We are manually building the Document Object Model (DOM) tree structure in JSON. { type: 'text', text: content }. Now, if content is <b>bold</b>, the editor renders the literal characters < b > rather than making the text bold.

The Exploit: Social Engineering with Style

So, we have HTML injection. "But wait," you say, "TipTap sanitizes input! You can't just inject <script> tags!" You are correct. If you try to inject <script>alert(1)</script>, TipTap's sanitizer (usually based on the Schema) will likely strip it out. We aren't getting a low-effort XSS here. But we don't need JavaScript to ruin someone's day.

We have HTML Injection. We can inject <a> tags, header tags (<h1>), and dangerously, <svg> tags (depending on the schema configuration). This allows for Content Spoofing. An attacker can craft a link to a legitimate Vikunja instance that, when clicked, renders a fake UI overlay.

The Attack Scenario:

  1. Craft the URL: The attacker constructs a URL. https://tasks.corp.com/projects/1?filter=<h3>Session+Expired</h3><a+href='https://evil.com/login'>Click+here+to+re-authenticate</a>.
  2. The Delivery: Send this link to the CEO via Slack. "Hey, the project board is acting up, can you check this link?"
  3. The Trap: The CEO clicks. The page loads. The TipTap editor parses the filter. Instead of seeing the text Session Expired in the input box, they see a rendered Header and a clickable link that looks native to the application styling.

Because the link is hosted on the trusted domain (tasks.corp.com), the victim's guard is down. This is effectively a legitimate-looking defacement that only exists for that specific user in that specific moment. It’s perfect for credential harvesting.

The Impact: Why 'Medium' Severity is Misleading

CVSS gives this a 6.1. It calls it 'Medium'. In the sterile world of vulnerability scoring, that makes sense. It requires user interaction (UI:R), and the scope change (S:C) is limited because we aren't popping full shells or stealing cookies directly via JS (yet). But in the real world, this is a high-risk vector for initial access.

Consider the context: Vikunja is a productivity tool. It contains sensitive project data, deadlines, and potentially API keys or secrets in task descriptions. If an attacker can successfully phish a user via this method, they gain valid credentials. From there, they can pivot to data exfiltration or internal sabotage.

Furthermore, while <script> is blocked by default, sanitizers are notoriously difficult to get right. If there is any allowed tag attribute that accepts a javascript: pseudo-protocol (like a lax href on an anchor tag) or an event handler that slips through the ProseMirror schema, this elevates instantly from HTML Injection to full Reflected XSS. Even without JS, the ability to fundamentally alter the visual trust of the application page is powerful. It breaks the 'What You See Is What You Get' contract of the web.

The Fix: Explicit Typing Saves Lives

The remediation here is straightforward, as seen in the patch analysis. If you are running Vikunja self-hosted, stop what you are doing and pull the 2.0.0 container image. The update fundamentally changes the data flow for the filter component.

For developers reading this, the lesson is broader than just Vikunja or TipTap. Never rely on implicit parsing of user input. If an API accepts a string or an object, and the string path triggers a parser (Markdown, HTML, XML), you are walking into a minefield. Always choose the structured, typed path.

If you cannot update for some reason (maybe you really love living on the edge), you could attempt to block the attack at the WAF level by filtering query parameters containing HTML tags (<, >), but that's a game of whack-a-mole you will eventually lose. The only true fix is the code change: force the editor to treat the input as a text node, rendering the HTML tags as harmless strings rather than executable DOM elements.

Official Patches

VikunjaGitHub Commit fixing the issue

Fix Analysis (1)

Technical Appendix

CVSS Score
6.1/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

Affected Systems

Vikunja (Self-hosted)Vikunja Frontend

Affected Versions Detail

Product
Affected Versions
Fixed Version
Vikunja
Vikunja
< 2.0.02.0.0
AttributeDetail
CWE IDCWE-79
Attack VectorNetwork
CVSS Score6.1 (Medium)
ImpactContent Spoofing / Phishing
VulnerabilityReflected HTML Injection
ComponentFilterInput.vue / TipTap Editor

MITRE ATT&CK Mapping

T1189Drive-by Compromise
Initial Access
T1566Phishing
Initial Access
T1204User Execution
Execution
CWE-79
Cross-site Scripting

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

Known Exploits & Detection

N/AExploit involves crafting a URL with HTML tags in the 'filter' query parameter.
NucleiDetection Template Available

Vulnerability Timeline

Vulnerability Disclosed (GHSA)
2026-02-25
CVE-2026-27116 Assigned
2026-02-25
Patch Released (v2.0.0)
2026-02-25

References & Sources

  • [1]GitHub Advisory
  • [2]Vikunja 2.0.0 Release Notes

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.