Files
sori.studio/lib/markdown-html-block.js
zenn de767095ba v1.10.88 품질·보안 개선과 미디어 카탈로그 database 전환을 반영한다.
세션·로그인 제한, ZIP/업로드 스트리밍, CSP 최소 강제와 Report-Only, Playwright 격리 검증을 추가하고 미디어 목록을 reconcile 후 database 모드로 고정한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 11:05:40 +09:00

100 lines
1.9 KiB
JavaScript

import sanitizeHtml from 'sanitize-html'
const ALLOWED_HTML_BLOCK_TAGS = [
'a',
'abbr',
'b',
'blockquote',
'br',
'caption',
'code',
'col',
'colgroup',
'dd',
'details',
'div',
'dl',
'dt',
'em',
'figcaption',
'figure',
'h2',
'h3',
'h4',
'h5',
'h6',
'hr',
'i',
'img',
'li',
'mark',
'ol',
'p',
'pre',
's',
'small',
'span',
'strong',
'sub',
'summary',
'sup',
'table',
'tbody',
'td',
'tfoot',
'th',
'thead',
'time',
'tr',
'u',
'ul'
]
const HTML_BLOCK_SANITIZE_OPTIONS = {
allowedTags: ALLOWED_HTML_BLOCK_TAGS,
allowedAttributes: {
'*': ['class', 'title', 'aria-label', 'aria-hidden'],
a: ['href', 'target', 'rel'],
col: ['span', 'width'],
img: ['src', 'alt', 'width', 'height', 'loading'],
li: ['value'],
ol: ['start', 'type'],
td: ['colspan', 'rowspan', 'headers'],
th: ['colspan', 'rowspan', 'headers', 'scope'],
time: ['datetime']
},
allowedSchemes: ['http', 'https', 'mailto', 'tel'],
allowedSchemesAppliedToAttributes: ['href', 'src'],
allowProtocolRelative: false,
nonTextTags: ['script', 'style', 'textarea', 'option', 'template', 'noscript'],
transformTags: {
a: (tagName, attribs) => {
if (attribs.target !== '_blank') {
return { tagName, attribs }
}
const relValues = new Set(String(attribs.rel || '').split(/\s+/).filter(Boolean))
relValues.add('noopener')
relValues.add('noreferrer')
return {
tagName,
attribs: {
...attribs,
rel: [...relValues].join(' ')
}
}
}
}
}
/**
* 본문 HTML 블록에서 실행성 코드와 위험 속성을 제거한다.
* @param {string} value - 원본 HTML
* @returns {string} 정리된 HTML
*/
export const sanitizeHtmlBlock = (value) => {
const html = String(value || '').trim()
return html ? sanitizeHtml(html, HTML_BLOCK_SANITIZE_OPTIONS).trim() : ''
}