Built-in

Security

Sanitize the parsed AST by removing dangerous elements, blocking malicious protocols, and restricting link destinations.

The comark/plugins/security plugin sanitizes the parsed AST, removing dangerous HTML elements, blocking malicious protocols, and restricting allowed link destinations.

Usage

import { parse } from 'comark'
import security from 'comark/plugins/security'

const result = await parse(content, {
  plugins: [security()]
})

With framework components:

<script setup lang="ts">
import { Comark } from '@comark/vue'
import security from '@comark/vue/plugins/security'

const plugins = [
  security({
    blockedTags: ['script', 'iframe'],
    allowedProtocols: ['https', 'mailto']
  })
]
</script>

<template>
  <Comark :plugins="plugins">{{ content }}</Comark>
</template>

Features

Several sanitizations are applied automatically and cannot be disabled:

Event Handlers

All on* attributes are stripped regardless of case — onclick, onerror, onload, onmouseover, and any other on* attribute.

<div onclick="alert('XSS')">Click me</div>
<img src="x" onerror="alert('XSS')">

Dangerous Attributes

Attributes that can be abused regardless of value are always stripped:

AttributeRisk
srcdocCan contain arbitrary HTML
formactionCan redirect form submissions

Protocol Blocking

href and src values are decoded (URL-encoded and HTML entity variants included) and checked against a hard-coded block list. These protocols are always blocked, even if allowedProtocols: ['*'] is set:

javascript: · vbscript: · data:text/html · data:text/javascript · data:text/vbscript · data:text/css · data:text/plain · data:text/xml

<a href="javascript:alert('XSS')">Click</a>
<img src="data:text/html,<script>alert('XSS')</script>">

API

security(options?)

Returns a ComarkPlugin that sanitizes the parsed AST.

Parameters:

  • options? - Optional configuration — see Options

Returns: ComarkPlugin


Options

OptionTypeDefaultDescription
blockedTagsstring[][]Tag names to remove entirely from the AST
allowedProtocolsstring[]['*']Protocols permitted in href and src
allowedLinkPrefixesstring[]['*']URL prefixes permitted in href
allowedImagePrefixesstring[]['*']URL prefixes permitted in src
defaultOriginstringundefinedRewrite disallowed URLs to this origin instead of stripping
allowDataImagesbooleantrueAllow data:image/* URIs in src

blockedTags

Tag names to completely remove from the AST. Matching is case-insensitive, so SCRIPT, Script, and script are all caught.

security({
  blockedTags: ['script', 'iframe', 'object', 'embed', 'link', 'style']
})
TagRisk
scriptJavaScript execution
iframeLoads external content
objectEmbeds plugins or Flash
embedSimilar to object
linkLoads external stylesheets
styleCSS with javascript: expressions
baseChanges base URL for relative links
metaHTTP refresh / redirect

allowedProtocols

Restricts which URL protocols are permitted in href and src attributes. Use ['*'] to allow all protocols not already on the hard-coded block list.

security({
  allowedProtocols: ['https', 'mailto']
})
The hard-coded unsafe protocols (javascript:, vbscript:, data:text/*) are a floor that cannot be overridden — even allowedProtocols: ['javascript'] will not unblock javascript: URLs.

allowedLinkPrefixes

Restricts which URLs are allowed in href attributes. Relative URLs (starting with /, #, etc.) are always allowed regardless of this setting.

When a URL does not match any prefix and defaultOrigin is set, the URL is rewritten instead of stripped.

security({
  allowedLinkPrefixes: ['https://myapp.com', 'https://docs.myapp.com']
})

allowedImagePrefixes

Same as allowedLinkPrefixes but applies to src attributes only. The two options are checked independently — restricting one does not affect the other.

security({
  allowedImagePrefixes: ['https://cdn.myapp.com']
})

defaultOrigin

When a URL fails the allowedLinkPrefixes or allowedImagePrefixes check, it is rewritten to use this origin instead of being stripped. The path, query, and fragment of the original URL are preserved.

security({
  allowedLinkPrefixes: ['https://myapp.com'],
  defaultOrigin: 'https://myapp.com'
})
// https://evil.com/path → https://myapp.com/path

allowDataImages

Controls whether data:image/* URIs are allowed in src attributes. Set to false to block base64-encoded images, which can be used as tracking pixels or embedded payloads.

security({
  allowDataImages: false
})
data:text/* variants in href are always blocked by the hard-coded protocol list regardless of this setting.

Examples

User-Generated Content

The most common use case — lock down everything that could execute code or phone home:

import { parse } from 'comark'
import security from 'comark/plugins/security'

const result = await parse(userInput, {
  plugins: [
    security({
      blockedTags: ['script', 'iframe', 'object', 'embed', 'link', 'style'],
      allowedProtocols: ['https', 'mailto'],
      allowDataImages: false
    })
  ]
})

Keep all links and images within your own infrastructure, rewriting external URLs instead of stripping them:

security({
  allowedLinkPrefixes: ['https://myapp.com', 'https://docs.myapp.com'],
  allowedImagePrefixes: ['https://cdn.myapp.com'],
  defaultOrigin: 'https://myapp.com'
})

Block External Images

Prevent tracking pixels and externally-hosted images while keeping everything else permissive:

security({
  allowedImagePrefixes: ['https://cdn.myapp.com'],
  allowDataImages: false
})

Best Practices

Block tags, not just attributes

Blocking only <script> may not be enough — <iframe>, <object>, <embed>, <link>, and <style> can also execute or load external content:

// ✅ More thorough
security({
  blockedTags: ['script', 'iframe', 'object', 'embed', 'link', 'style']
})

// ⚠️ Incomplete
security({
  blockedTags: ['script']
})

Sanitize before storage

Sanitizing at parse time on read means malicious content already made it into the database. Sanitize before writing instead:

// ✅ Sanitize before storing
async function saveArticle(content: string) {
  const sanitized = await parse(content, {
    plugins: [security({ blockedTags: ['script', 'iframe'] })]
  })
  await db.articles.create({ content: sanitized })
}

Pair with a Content Security Policy

The plugin sanitizes the AST, but a CSP header adds a second line of defense in the browser:

// Express.js
res.setHeader(
  'Content-Security-Policy',
  "default-src 'self'; script-src 'none';"
)
The plugin runs during the post phase and traverses the AST once — O(n) in the number of nodes, with no impact on render time.