/**
* @bfra.me/doc-sync/utils/sanitization - Sanitization utilities for MDX content
* Provides comprehensive XSS prevention for user-generated content
*/
import {sanitizeInput} from '@bfra.me/es/validation'
import escapeHtml from 'escape-html'
/**
* Sanitize HTML content for MDX context
* Escapes all HTML entities and JSX curly braces to prevent XSS
*
* @param content - The content to sanitize
* @returns Sanitized content safe for MDX rendering
*
* @example
* ```ts
* const safe = sanitizeForMDX('')
* // Returns: '<script>alert("xss")</script>'
* ```
*/
export function sanitizeForMDX(content: string): string {
// Use existing sanitizeInput from @bfra.me/es/validation
// This escapes: & < > " ' /
const escaped = sanitizeInput(content, {trim: false})
// Additionally escape JSX curly braces for MDX safety
return escaped.replaceAll('{', '{').replaceAll('}', '}')
}
/**
* Sanitize value for use in HTML/JSX attribute
* Uses escape-html library for proper attribute encoding
*
* @param value - The attribute value to sanitize
* @returns Sanitized value safe for attribute context
*
* @example
* ```ts
* const safe = sanitizeAttribute('value" onload="alert(1)')
* // Returns: 'value" onload="alert(1)'
* ```
*/
export function sanitizeAttribute(value: string): string {
return escapeHtml(value)
}
/**
* JSX attribute parsed from a tag
*/
interface JSXAttribute {
readonly name: string
readonly value: string | null
}
/**
* Parse JSX tag attributes safely without using complex regex
* Uses a simple state machine approach to avoid ReDoS vulnerabilities
*
* @param tag - The complete JSX tag string (e.g., '')
* @returns Array of parsed attributes
*
* @example
* ```ts
* const attrs = parseJSXAttributes('')
* // Returns: [{name: 'title', value: 'Hello'}, {name: 'icon', value: 'star'}]
* ```
*/
export function parseJSXAttributes(tag: string): readonly JSXAttribute[] {
const attrs: JSXAttribute[] = []
const spaceIndex = tag.indexOf(' ')
if (spaceIndex === -1) return attrs
const closeIndex = tag.lastIndexOf('>')
if (closeIndex === -1) return attrs
const attrRegion = tag.slice(spaceIndex + 1, closeIndex).trim()
let i = 0
while (i < attrRegion.length) {
while (i < attrRegion.length && /\s/.test(attrRegion.charAt(i))) i++
if (i >= attrRegion.length) break
let name = ''
while (i < attrRegion.length && /[\w-]/.test(attrRegion.charAt(i))) {
name += attrRegion[i]
i++
}
if (!name) break
while (i < attrRegion.length && /\s/.test(attrRegion.charAt(i))) i++
if (i >= attrRegion.length || attrRegion[i] !== '=') {
attrs.push({name, value: null})
continue
}
i++
while (i < attrRegion.length && /\s/.test(attrRegion.charAt(i))) i++
if (i >= attrRegion.length) {
attrs.push({name, value: ''})
break
}
let value = ''
const quote = attrRegion[i]
if (quote === '"' || quote === "'") {
i++
while (i < attrRegion.length && attrRegion[i] !== quote) {
value += attrRegion[i]
i++
}
if (i < attrRegion.length) i++
} else {
while (i < attrRegion.length && !/[\s/>]/.test(attrRegion.charAt(i))) {
value += attrRegion[i]
i++
}
}
attrs.push({name, value})
}
return attrs
}
/**
* Sanitize a complete JSX tag including all attributes
* Parses the tag and escapes all attribute values to prevent XSS
*
* @param tag - The complete JSX tag string
* @returns Sanitized JSX tag safe for rendering
*
* @example
* ```ts
* const safe = sanitizeJSXTag('')
* // Returns: '' (with escaped values)
* ```
*/
export function sanitizeJSXTag(tag: string): string {
// Extract tag name
const tagMatch = tag.match(/^<([A-Z][a-zA-Z0-9]*)/)
if (!tagMatch || typeof tagMatch[1] !== 'string' || tagMatch[1].length === 0) {
// Not a valid JSX tag, escape everything
return escapeHtml(tag)
}
const tagName = tagMatch[1]
const selfClosing = tag.endsWith('/>')
const attributes = parseJSXAttributes(tag)
// Sanitize each attribute value
const sanitizedAttrs = attributes.map(({name, value}) => {
if (value === null) return name
const escaped = escapeHtml(value)
return `${name}="${escaped}"`
})
const attrString = sanitizedAttrs.length > 0 ? ` ${sanitizedAttrs.join(' ')}` : ''
return `<${tagName}${attrString}${selfClosing ? ' />' : '>'}`
}