Skip to content

allowedSchemes includes data, so data:text/html links survive sanitization (stored/preview XSS) #1176

Description

@godownio

Summary

Artalk’s frontend HTML sanitizer allowlists the data: URL scheme. Comment HTML such as <a href="data:text/html;base64,…"> therefore survives sanitization and is injected via innerHTML. Clicking the link executes arbitrary JavaScript in a data: browsing context.

This is present in v2.9.1 (insane) and still present in v2.10.0 after the sanitizer was switched to DOMPurify. v2.10.0’s release notes say the previous URL-scheme allowlist was intentionally retained, so upgrading to 2.10.0 does not close this issue.

Source (v2.9.1 ui/artalk/src/lib/sanitizer.ts):

allowedSchemes: [
  'http',
  'https',
  'mailto',
  'data', // for support base64 encoded image (安全性有待考虑)
],

v2.10.0 still has:

const allowedSchemes = new Set(['http', 'https', 'mailto', 'data'])
// …
ADD_DATA_URI_TAGS: ['a'],

The comment already notes that allowing data is a security concern. The intended use appears to be base64 images, but the allowlist does not restrict MIME type, so data:text/html on <a href> is accepted.

<script> tags are stripped (allowlist does not include script). The bypass is the allowed data: scheme on a[href].

Affected versions

  • Confirmed in source: v2.9.1 (ui/artalk/src/lib/sanitizer.ts + insane@2.6.2)
  • Confirmed in source: v2.10.0 (DOMPurify, same data scheme + ADD_DATA_URI_TAGS: ['a'])
  • v2.10.0 tests still expect <a href="data:text/plain,hello"> to be kept

Reproduction (local, sanitizer only)

Using the v2.9.1 insane options copied from sanitizer.ts:

import insane from 'insane'

const insaneOptions = {
  allowedSchemes: ['http', 'https', 'mailto', 'data'],
  allowedTags: ['a', 'p', 'img' /* …same as Artalk… */],
  allowedAttributes: {
    a: ['href', 'name', 'target', 'aria-label', 'rel'],
    img: ['src', 'alt', 'title'],
  },
}

const payload =
  '<p><a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==" target="_blank">click</a></p>'

console.log(insane(payload, insaneOptions))
// → <p><a href="data:text/html;base64,…" target="_blank">click</a></p>
// data: survives

console.log(insane(payload, { ...insaneOptions, allowedSchemes: ['http', 'https', 'mailto'] }))
// → <p><a target="_blank">click</a></p>
// href stripped

Base64 payload decodes to <script>alert(1)</script>.

In the live UI: put that HTML in the comment editor and open Preview. Artalk runs marked then sanitize() / getContentMarked() into innerHTML. A data:text/html link appears; clicking it (often target="_blank") executes the script.

Impact

  • Unauthenticated comment content (or preview of unsaved content) can carry a data:text/html link.
  • Clicking the link runs attacker JavaScript. That document’s origin is "null" (data: URLs), so this is not classic same-origin cookie theft from the host site (document.cookie on data: pages is blocked).
  • Practical impact: phishing / fake “support” pages, arbitrary JS in the data: document, social-engineering of moderators who click links while reviewing comments.
  • <img src="https://…"> in comments can also fire automatically when HTML is rendered (OOB ping), which is a separate issue from JS execution.

Suggested fix

  1. Remove 'data' from allowedSchemes, or only allow data: on <img src> with an image MIME (data:image/png, data:image/jpeg, data:image/gif, data:image/webp) and never on <a href>.
  2. Explicitly reject data:text/html, data:text/javascript, data:application/javascript, and data:image/svg+xml.
  3. Drop ADD_DATA_URI_TAGS: ['a'] in the DOMPurify config.
  4. Add a regression test: sanitize('<a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==">x</a>') must not keep that href.
  5. Server-side: do not trust client HTML; sanitize content the same way so pending/moderator views cannot render raw data:text/html.

v2.10.0 replacing insane with DOMPurify is a good hardening step for ReDoS / unmaintained parser issues; it does not by itself fix this allowlist bug.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions