@iyulab/chat-components 0.11.4 → 0.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.5] - 2026-08-31
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
|
|
7
|
+
- **`UMarkedBlock` rendered unsanitized markdown output.** `marked` dropped
|
|
8
|
+
its built-in `sanitize` option in v5 and leaves that entirely to the
|
|
9
|
+
consumer; this component piped `marked`'s output straight into
|
|
10
|
+
`unsafeHTML()` without ever making that call. Raw HTML embedded in
|
|
11
|
+
markdown source (`<img onerror>`, `<script>`) ran verbatim, and
|
|
12
|
+
`javascript:`/`data:`/`vbscript:` hrefs in ordinary links, images, and
|
|
13
|
+
autolinks passed through untouched. Table cells hit the same gap
|
|
14
|
+
through a second path (`renderTable`'s inline parser call was missing
|
|
15
|
+
its options, silently falling back to `marked`'s unconfigured default
|
|
16
|
+
renderer). Fenced-code-block content had the same problem one layer
|
|
17
|
+
down — it reached `unsafeHTML()` unescaped even though a comment
|
|
18
|
+
already claimed otherwise.
|
|
19
|
+
Fixed by adding renderer overrides built on this package's existing
|
|
20
|
+
HTML/href sanitizers (no new dependency) and closing both bypass
|
|
21
|
+
paths.
|
|
22
|
+
|
|
3
23
|
## [0.11.4] - 2026-08-27
|
|
4
24
|
|
|
5
25
|
### Fixed
|
|
@@ -7,6 +7,15 @@ import { ReferenceCitation } from '../../types/References.js';
|
|
|
7
7
|
* 주의:
|
|
8
8
|
* - refs로 삽입되는 커스텀 태그(u-ref-*)는 속성/내용을 모두 escape하여 XSS를 방지합니다.
|
|
9
9
|
* - 코드블록 내부는 HTML을 전부 텍스트로 취급(escape)하며, refs 태그는 통째 제거합니다.
|
|
10
|
+
* - 마크다운 소스에 섞인 원시 HTML(html 토큰, 블록/인라인 공통)은 렌더하지 않고
|
|
11
|
+
* 텍스트로 escape합니다 — marked는 v5부터 sanitize 옵션을 제거했고 그 판단을
|
|
12
|
+
* 소비자에게 위임하므로, 이 컴포넌트가 직접 책임집니다.
|
|
13
|
+
* - 마크다운 링크/이미지(`[..](href)`, ``, autolink)의 href/src는
|
|
14
|
+
* `escapeHtmlHref`로 protocol을 검사합니다(javascript:/data:/vbscript: 차단) —
|
|
15
|
+
* marked 자신은 URL을 encodeURI할 뿐 protocol을 걸러내지 않습니다.
|
|
16
|
+
* - 테이블 셀 인라인 콘텐츠(renderTable)도 위 두 규칙과 같은 렌더러 설정을 공유합니다
|
|
17
|
+
* — `Parser.parseInline`을 옵션 없이 호출하면 기본(비-sanitize) 렌더러로 되돌아가므로
|
|
18
|
+
* 반드시 `this.parser.defaults`를 함께 넘겨야 합니다.
|
|
10
19
|
* - 인덱스 기반 삽입(ref.endIndex)이 깨지지 않도록 "normalize(제로폭 제거)"를 insertRefs 전에 1회 수행합니다.
|
|
11
20
|
*/
|
|
12
21
|
export declare class UMarkedBlock extends UElement {
|
|
@@ -32,8 +41,27 @@ export declare class UMarkedBlock extends UElement {
|
|
|
32
41
|
/**
|
|
33
42
|
* 테이블은 JSON 데이터로 변환하여 별도의 컴포넌트로 렌더링합니다.
|
|
34
43
|
* 셀 내부의 인라인 마크다운(볼드, 코드, br 등)도 HTML로 변환합니다.
|
|
44
|
+
*
|
|
45
|
+
* `Parser.parseInline`은 옵션을 생략하면 marked 기본(비-sanitize) 렌더러로
|
|
46
|
+
* 돌아가 이 컴포넌트의 `html`/`link`/`image` 오버라이드를 건너뛴다 — 셀 안의
|
|
47
|
+
* 원시 HTML이나 `javascript:` 링크가 그대로 통과하는 별도 XSS 경로가 되므로,
|
|
48
|
+
* 반드시 `this.parser.defaults`(우리 렌더러가 병합된 설정)를 함께 넘긴다.
|
|
35
49
|
*/
|
|
36
50
|
private renderTable;
|
|
51
|
+
/**
|
|
52
|
+
* 마크다운 소스에 섞인 원시 HTML(블록/인라인 공통 — marked는 두 경우 모두
|
|
53
|
+
* 같은 `html` 렌더러 훅을 쓴다)을 렌더하지 않고 텍스트로 escape합니다.
|
|
54
|
+
* marked v5+는 sanitize 옵션을 제거하고 이 판단을 소비자에게 위임했다.
|
|
55
|
+
*/
|
|
56
|
+
private renderHtml;
|
|
57
|
+
/**
|
|
58
|
+
* 마크다운 링크의 href는 `escapeHtmlHref`로 protocol을 검사합니다
|
|
59
|
+
* (javascript:/data:/vbscript: 등은 `#`로 무력화). marked 기본 렌더러는
|
|
60
|
+
* URL을 encodeURI할 뿐 protocol을 걸러내지 않는다.
|
|
61
|
+
*/
|
|
62
|
+
private renderLink;
|
|
63
|
+
/** 마크다운 이미지의 src도 `renderLink`와 동일하게 protocol을 검사합니다. */
|
|
64
|
+
private renderImage;
|
|
37
65
|
/**
|
|
38
66
|
* 인용/출처 참조객체가 있을때 ref 태그 HTML을 this.placeholder에 보관하고
|
|
39
67
|
* 마크다운 문자열의 해당 위치에 주석 플레이스홀더를 삽입합니다.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateMetadata.js";
|
|
2
2
|
import __decorate from "../../_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorate.js";
|
|
3
3
|
import "./UCodeBlock.js";
|
|
4
|
-
import { escapeHtmlText, stripZeroWidth } from "../../utilities/sanitizers.js";
|
|
4
|
+
import { escapeHtmlAttr, escapeHtmlHref, escapeHtmlText, stripZeroWidth } from "../../utilities/sanitizers.js";
|
|
5
5
|
import { HtmlBuilder } from "../../utilities/HtmlBuilder.js";
|
|
6
6
|
import { UElementBlock } from "./UElementBlock.js";
|
|
7
7
|
import "../references/URefTag.js";
|
|
@@ -27,7 +27,10 @@ var UMarkedBlock = class UMarkedBlock extends UElement {
|
|
|
27
27
|
silent: true,
|
|
28
28
|
renderer: {
|
|
29
29
|
code: (token) => this.renderCode(token),
|
|
30
|
-
table: (token) => this.renderTable(token)
|
|
30
|
+
table: (token) => this.renderTable(token),
|
|
31
|
+
html: (token) => this.renderHtml(token),
|
|
32
|
+
link: (token) => this.renderLink(token),
|
|
33
|
+
image: (token) => this.renderImage(token)
|
|
31
34
|
}
|
|
32
35
|
}).use(markedKatex({ output: "mathml" }));
|
|
33
36
|
this.placeholder = new HtmlPlaceholder();
|
|
@@ -77,20 +80,25 @@ var UMarkedBlock = class UMarkedBlock extends UElement {
|
|
|
77
80
|
renderCode(token) {
|
|
78
81
|
const lang = token.lang ?? "plaintext";
|
|
79
82
|
if (lang === "block-json") return UElementBlock.buildHTML(token.text, { loading: this.streaming });
|
|
80
|
-
const safeText = this.removeRefs(token.text);
|
|
83
|
+
const safeText = escapeHtmlText(this.removeRefs(token.text));
|
|
81
84
|
return HtmlBuilder.build("u-code-block", { lang }, safeText);
|
|
82
85
|
}
|
|
83
86
|
/**
|
|
84
87
|
* 테이블은 JSON 데이터로 변환하여 별도의 컴포넌트로 렌더링합니다.
|
|
85
88
|
* 셀 내부의 인라인 마크다운(볼드, 코드, br 등)도 HTML로 변환합니다.
|
|
89
|
+
*
|
|
90
|
+
* `Parser.parseInline`은 옵션을 생략하면 marked 기본(비-sanitize) 렌더러로
|
|
91
|
+
* 돌아가 이 컴포넌트의 `html`/`link`/`image` 오버라이드를 건너뛴다 — 셀 안의
|
|
92
|
+
* 원시 HTML이나 `javascript:` 링크가 그대로 통과하는 별도 XSS 경로가 되므로,
|
|
93
|
+
* 반드시 `this.parser.defaults`(우리 렌더러가 병합된 설정)를 함께 넘긴다.
|
|
86
94
|
*/
|
|
87
95
|
renderTable(token) {
|
|
88
96
|
const headers = token.header.map((h) => ({
|
|
89
|
-
text: h.tokens ? Parser.parseInline(h.tokens) : h.text,
|
|
97
|
+
text: h.tokens ? Parser.parseInline(h.tokens, this.parser.defaults) : h.text,
|
|
90
98
|
align: h.align
|
|
91
99
|
}));
|
|
92
100
|
const rows = token.rows.map((row) => row.map((cell) => ({
|
|
93
|
-
text: cell.tokens ? Parser.parseInline(cell.tokens) : cell.text,
|
|
101
|
+
text: cell.tokens ? Parser.parseInline(cell.tokens, this.parser.defaults) : cell.text,
|
|
94
102
|
align: cell.align
|
|
95
103
|
})));
|
|
96
104
|
return UTableBlock.buildHTML({
|
|
@@ -99,6 +107,33 @@ var UMarkedBlock = class UMarkedBlock extends UElement {
|
|
|
99
107
|
});
|
|
100
108
|
}
|
|
101
109
|
/**
|
|
110
|
+
* 마크다운 소스에 섞인 원시 HTML(블록/인라인 공통 — marked는 두 경우 모두
|
|
111
|
+
* 같은 `html` 렌더러 훅을 쓴다)을 렌더하지 않고 텍스트로 escape합니다.
|
|
112
|
+
* marked v5+는 sanitize 옵션을 제거하고 이 판단을 소비자에게 위임했다.
|
|
113
|
+
*/
|
|
114
|
+
renderHtml(token) {
|
|
115
|
+
return escapeHtmlText(token.text);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 마크다운 링크의 href는 `escapeHtmlHref`로 protocol을 검사합니다
|
|
119
|
+
* (javascript:/data:/vbscript: 등은 `#`로 무력화). marked 기본 렌더러는
|
|
120
|
+
* URL을 encodeURI할 뿐 protocol을 걸러내지 않는다.
|
|
121
|
+
*/
|
|
122
|
+
renderLink(token) {
|
|
123
|
+
const text = token.tokens ? Parser.parseInline(token.tokens, this.parser.defaults) : token.text;
|
|
124
|
+
let out = `<a href="${escapeHtmlHref(token.href)}"`;
|
|
125
|
+
if (token.title) out += ` title="${escapeHtmlAttr(token.title)}"`;
|
|
126
|
+
out += `>${text}</a>`;
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
/** 마크다운 이미지의 src도 `renderLink`와 동일하게 protocol을 검사합니다. */
|
|
130
|
+
renderImage(token) {
|
|
131
|
+
let out = `<img src="${escapeHtmlHref(token.href)}" alt="${escapeHtmlAttr(token.text)}"`;
|
|
132
|
+
if (token.title) out += ` title="${escapeHtmlAttr(token.title)}"`;
|
|
133
|
+
out += `>`;
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
102
137
|
* 인용/출처 참조객체가 있을때 ref 태그 HTML을 this.placeholder에 보관하고
|
|
103
138
|
* 마크다운 문자열의 해당 위치에 주석 플레이스홀더를 삽입합니다.
|
|
104
139
|
*/
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { UDataElement } from '../UDataElement.js';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* 테이블 셀 데이터 타입
|
|
4
|
+
*
|
|
5
|
+
* ⚠`text`는 렌더링 시 `unsafeHTML()`로 그대로 주입되는 **신뢰된 HTML 마크업**입니다
|
|
6
|
+
* (일반 텍스트가 아닙니다) — `UMarkedBlock`이 마크다운 인라인 렌더러(자체 XSS 방어
|
|
7
|
+
* 포함: 원시 HTML escape, `javascript:`/`data:` 링크 차단)를 거쳐 채우는 것을
|
|
8
|
+
* 전제로 합니다. 이 컴포넌트를 직접 구동하는 소비자가 정화되지 않은 외부 문자열을
|
|
9
|
+
* `text`에 넣으면 XSS로 이어질 수 있습니다 — 반드시 사전에 sanitize할 것.
|
|
10
|
+
*/
|
|
3
11
|
export interface TableCell {
|
|
4
|
-
/** 셀에 표시할
|
|
12
|
+
/** 셀에 표시할 (이미 안전 처리된) HTML 마크업 */
|
|
5
13
|
text: string;
|
|
6
14
|
/** 셀 내용의 정렬 방식. 기본값은 "left" */
|
|
7
15
|
align: "left" | "center" | "right" | null;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iyulab/chat-components",
|
|
3
3
|
"description": "llm chat components for building chat interfaces",
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.5",
|
|
5
5
|
"author": "iyulab",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"./react/*": "./dist/react/*"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
|
+
"preversion": "node ../../scripts/preversion-check.mjs",
|
|
54
55
|
"start": "vite",
|
|
55
56
|
"build": "npm run typecheck && eslint && vite build",
|
|
56
57
|
"test": "vitest run",
|