Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
1113181a
authored
Feb 06, 2026
by
Calcium-Ion
Committed by
GitHub
Feb 06, 2026
Browse files
Options
Browse Files
Download
Plain Diff
Merge commit from fork
馃敀
fix(security): sanitize AI-generated HTML to prevent XSS in playground
parents
a9982ef2
708b7bef
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
82 additions
and
16 deletions
+82
-16
web/src/components/common/markdown/MarkdownRenderer.jsx
+44
-1
web/src/components/playground/CodeViewer.jsx
+38
-15
No files found.
web/src/components/common/markdown/MarkdownRenderer.jsx
View file @
1113181a
...
...
@@ -93,6 +93,49 @@ export function Mermaid(props) {
);
}
function
SandboxedHtmlPreview
({
code
})
{
const
iframeRef
=
useRef
(
null
);
const
[
iframeHeight
,
setIframeHeight
]
=
useState
(
150
);
useEffect
(()
=>
{
const
iframe
=
iframeRef
.
current
;
if
(
!
iframe
)
return
;
const
handleLoad
=
()
=>
{
try
{
const
doc
=
iframe
.
contentDocument
||
iframe
.
contentWindow
?.
document
;
if
(
doc
)
{
const
height
=
doc
.
documentElement
.
scrollHeight
||
doc
.
body
.
scrollHeight
;
setIframeHeight
(
Math
.
min
(
Math
.
max
(
height
+
16
,
60
),
600
));
}
}
catch
{
// sandbox restrictions may prevent access, that's fine
}
};
iframe
.
addEventListener
(
'load'
,
handleLoad
);
return
()
=>
iframe
.
removeEventListener
(
'load'
,
handleLoad
);
},
[
code
]);
return
(
<
iframe
ref=
{
iframeRef
}
sandbox=
'allow-same-origin'
srcDoc=
{
code
}
title=
'HTML Preview'
style=
{
{
width
:
'100%'
,
height
:
`${iframeHeight}px`
,
border
:
'none'
,
overflow
:
'auto'
,
backgroundColor
:
'#fff'
,
borderRadius
:
'4px'
,
}
}
/>
);
}
export
function
PreCode
(
props
)
{
const
ref
=
useRef
(
null
);
const
[
mermaidCode
,
setMermaidCode
]
=
useState
(
''
);
...
...
@@ -227,7 +270,7 @@ export function PreCode(props) {
>
HTML棰勮:
</
div
>
<
div
dangerouslySetInnerHTML=
{
{
__html
:
htmlCode
}
}
/>
<
SandboxedHtmlPreview
code=
{
htmlCode
}
/>
</
div
>
)
}
</>
...
...
web/src/components/playground/CodeViewer.jsx
View file @
1113181a
...
...
@@ -91,22 +91,45 @@ const codeThemeStyles = {
},
};
const
escapeHtml
=
(
str
)
=>
{
return
str
.
replace
(
/&/g
,
'&'
)
.
replace
(
/</g
,
'<'
)
.
replace
(
/>/g
,
'>'
)
.
replace
(
/"/g
,
'"'
)
.
replace
(
/'/g
,
'''
);
};
const
highlightJson
=
(
str
)
=>
{
return
str
.
replace
(
/
(
"
(\\
u
[
a-zA-Z0-9
]{4}
|
\\[^
u
]
|
[^\\
"
])
*"
(\s
*:
)?
|
\b(
true|false|null
)\b
|-
?\d
+
(?:\.\d
*
)?(?:[
eE
][
+-
]?\d
+
)?)
/g
,
(
match
)
=>
{
let
color
=
'#b5cea8'
;
if
(
/^"/
.
test
(
match
))
{
color
=
/:$/
.
test
(
match
)
?
'#9cdcfe'
:
'#ce9178'
;
}
else
if
(
/true|false|null/
.
test
(
match
))
{
color
=
'#569cd6'
;
}
return
`<span style="color:
${
color
}
">
${
match
}
</span>`
;
},
);
const
tokenRegex
=
/
(
"
(\\
u
[
a-zA-Z0-9
]{4}
|
\\[^
u
]
|
[^\\
"
])
*"
(\s
*:
)?
|
\b(
true|false|null
)\b
|-
?\d
+
(?:\.\d
*
)?(?:[
eE
][
+-
]?\d
+
)?)
/g
;
let
result
=
''
;
let
lastIndex
=
0
;
let
match
;
while
((
match
=
tokenRegex
.
exec
(
str
))
!==
null
)
{
// Escape non-token text (structural chars like {, }, [, ], :, comma, whitespace)
result
+=
escapeHtml
(
str
.
slice
(
lastIndex
,
match
.
index
));
const
token
=
match
[
0
];
let
color
=
'#b5cea8'
;
if
(
/^"/
.
test
(
token
))
{
color
=
/:$/
.
test
(
token
)
?
'#9cdcfe'
:
'#ce9178'
;
}
else
if
(
/true|false|null/
.
test
(
token
))
{
color
=
'#569cd6'
;
}
// Escape token content before wrapping in span
result
+=
`<span style="color:
${
color
}
">
${
escapeHtml
(
token
)}
</span>`
;
lastIndex
=
tokenRegex
.
lastIndex
;
}
// Escape remaining text
result
+=
escapeHtml
(
str
.
slice
(
lastIndex
));
return
result
;
};
const
linkRegex
=
/
(
https
?
:
\/\/
[^\s
<"'
\]
),;}
]
+
)
/g
;
const
linkRegex
=
/
(
https
?
:
\/\/
(?:[^\s
<"'
\]
),;&}
]
|&
)
+
)
/g
;
const
linkifyHtml
=
(
html
)
=>
{
const
parts
=
html
.
split
(
/
(
<
[^
>
]
+>
)
/g
);
...
...
@@ -184,14 +207,14 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
const
highlightedContent
=
useMemo
(()
=>
{
if
(
contentMetrics
.
isVeryLarge
&&
!
isExpanded
)
{
return
displayContent
;
return
escapeHtml
(
displayContent
)
;
}
if
(
isJsonLike
(
displayContent
,
language
))
{
return
highlightJson
(
displayContent
);
}
return
displayContent
;
return
escapeHtml
(
displayContent
)
;
},
[
displayContent
,
language
,
contentMetrics
.
isVeryLarge
,
isExpanded
]);
const
renderedContent
=
useMemo
(()
=>
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment