Skip to content

Commit b2f7e9b

Browse files
authored
Merge commit from fork
1 parent af7f499 commit b2f7e9b

File tree

3 files changed

+384
-2
lines changed

3 files changed

+384
-2
lines changed

marimo/_server/templates/templates.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,24 @@
2424
MOUNT_CONFIG_TEMPLATE = "'{{ mount_config }}'"
2525

2626

27+
_json_script_escapes = {
28+
ord(">"): "\\u003E",
29+
ord("<"): "\\u003C",
30+
ord("&"): "\\u0026",
31+
}
32+
33+
2734
def _html_escape(text: str) -> str:
2835
"""Escape HTML special characters."""
2936
return html.escape(text, quote=True)
3037

3138

39+
def json_script(data: Any) -> str:
40+
# See https://github.com/django/django/blob/main/django/utils/html.py#L88C1-L92C2
41+
# Only escape values that can break out of a script tag
42+
return json.dumps(data, sort_keys=True).translate(_json_script_escapes)
43+
44+
3245
def _get_mount_config(
3346
*,
3447
filename: Optional[str],
@@ -79,7 +92,7 @@ def _get_mount_config(
7992
"runtimeConfig": {runtime_config},
8093
}}
8194
""".format(
82-
**{k: json.dumps(v, sort_keys=True) for k, v in options.items()}
95+
**{k: json_script(v) for k, v in options.items()}
8396
).strip()
8497

8598

@@ -258,7 +271,7 @@ def static_notebook_template(
258271
f"""
259272
<script data-marimo="true">
260273
window.__MARIMO_STATIC__ = {{}};
261-
window.__MARIMO_STATIC__.files = {json.dumps(files)};
274+
window.__MARIMO_STATIC__.files = {json_script(files)};
262275
</script>
263276
"""
264277
)

tests/_server/templates/test_templates.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,119 @@ def test_static_notebook_template_with_nonexistent_custom_css(
701701
assert "<style title='marimo-custom'>" not in result
702702
_assert_no_leftover_replacements(result)
703703

704+
def test_static_files_injection_prevention(self) -> None:
705+
"""Test that malicious content in files dict doesn't enable script breakout."""
706+
# Test with malicious file keys and values
707+
malicious_files = {
708+
"normal.txt": "safe content",
709+
"</script><script>alert(1)</script>": "content",
710+
"file.js": "</script><img src=x onerror=alert(1)>",
711+
"test&<>.py": "content with <script>alert(1)</script>",
712+
}
713+
714+
result = templates.static_notebook_template(
715+
self.html,
716+
self.user_config,
717+
self.config_overrides,
718+
self.server_token,
719+
self.app_config,
720+
self.filepath,
721+
self.code,
722+
hash_code(self.code),
723+
self.session_snapshot,
724+
self.notebook_snapshot,
725+
malicious_files,
726+
)
727+
728+
# Must not contain unescaped script breakout sequences (< and > escaped)
729+
assert "</script><script>alert(1)" not in result
730+
assert "<img" not in result # The < should be escaped
731+
732+
# Must contain escaped versions of < and >
733+
assert "\\u003C" in result or "\\u003E" in result
734+
735+
# Must still be valid HTML
736+
_assert_no_leftover_replacements(result)
737+
738+
def test_static_malicious_filename_injection(self) -> None:
739+
"""Test that malicious filenames in static exports are properly escaped."""
740+
malicious_filepath = self.tmp_path / "</script><script>alert(1)</script>.py"
741+
742+
result = templates.static_notebook_template(
743+
self.html,
744+
self.user_config,
745+
self.config_overrides,
746+
self.server_token,
747+
self.app_config,
748+
str(malicious_filepath),
749+
self.code,
750+
hash_code(self.code),
751+
self.session_snapshot,
752+
self.notebook_snapshot,
753+
self.files,
754+
)
755+
756+
# Must not contain unescaped script tags
757+
assert "</script><script>" not in result
758+
assert "<script>alert(1)" not in result.replace("\\u003Cscript\\u003E", "")
759+
760+
# Must contain escaped versions in JSON context
761+
assert "\\u003C" in result or "\\u003E" in result
762+
763+
_assert_no_leftover_replacements(result)
764+
765+
def test_static_malicious_code_content(self) -> None:
766+
"""Test that malicious code content is properly escaped in static export."""
767+
malicious_code = """
768+
import marimo as mo
769+
# This code contains </script><script>alert('XSS')</script>
770+
mo.md("<img src=x onerror=alert(1)>")
771+
"""
772+
773+
# Create session with malicious code in output
774+
malicious_session = NotebookSessionV1(
775+
version=VERSION,
776+
metadata=NotebookSessionMetadata(marimo_version="0.1.0"),
777+
cells=[
778+
Cell(
779+
cell_id="cell1",
780+
code=malicious_code,
781+
outputs=[
782+
DataOutput(
783+
channel="output",
784+
mimetype="text/html",
785+
data="</script><script>alert(1)</script>",
786+
timestamp=0.0,
787+
)
788+
],
789+
console=[],
790+
)
791+
],
792+
)
793+
794+
result = templates.static_notebook_template(
795+
self.html,
796+
self.user_config,
797+
self.config_overrides,
798+
self.server_token,
799+
self.app_config,
800+
self.filepath,
801+
malicious_code,
802+
hash_code(malicious_code),
803+
malicious_session,
804+
self.notebook_snapshot,
805+
self.files,
806+
)
807+
808+
# The malicious code itself should be in JSON context (escaped)
809+
# Must not have unescaped dangerous sequences in script tags (< and > escaped)
810+
assert "</script><script>alert('XSS')" not in result
811+
812+
# Must have escaped versions of < and >
813+
assert "\\u003C" in result or "\\u003E" in result
814+
815+
_assert_no_leftover_replacements(result)
816+
704817

705818
class TestWasmNotebookTemplate(unittest.TestCase):
706819
def setUp(self) -> None:

0 commit comments

Comments
 (0)