Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions dargs/notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,11 +259,11 @@ def print_html(self, _level: int = 0, _last_one: bool = True) -> str:
buff.append(r"""<code class="dargs-code">""")
buff.append('"')
if isinstance(self.arg, Argument):
buff.append(self.arg.name)
buff.append(html.escape(self.arg.name, quote=False))
elif isinstance(self.arg, Variant):
buff.append(self.arg.flag_name)
buff.append(html.escape(self.arg.flag_name, quote=False))
elif isinstance(self.arg, str):
buff.append(self.arg)
buff.append(html.escape(self.arg, quote=False))
else:
raise ValueError(f"Unknown type: {type(self.arg)}")
buff.append('"')
Expand All @@ -276,6 +276,9 @@ def print_html(self, _level: int = 0, _last_one: bool = True) -> str:
.replace("| type:", "type:")
.replace("\n", linebreak)
)
# Escape generated text before selectively restoring the small
# set of formatting markers supported by the tooltip.
doc_head = html.escape(doc_head, quote=False)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Escape before inserting the trusted line-break markup

doc_head has already converted newlines into <br/> before this call, so html.escape() turns every intended tooltip line break into visible &lt;br/&gt; text. I reproduced output such as child: &lt;br/&gt; type: .... Recompute from the unformatted generated text, escape it, and only then add renderer-owned line breaks:

Suggested change
doc_head = html.escape(doc_head, quote=False)
doc_head = html.escape(
self.arg.gen_doc_head().replace("| type:", "type:"),
quote=False,
).replace("\n", linebreak)

After applying this suggestion, the earlier doc_head assignment at lines 274–278 is redundant and should be removed.

# use re to replace ``xx`` to <code>xx</code>
doc_head = re.sub(
r"``(.*?)``",
Expand All @@ -285,19 +288,21 @@ def print_html(self, _level: int = 0, _last_one: bool = True) -> str:
doc_head = re.sub(r"\*(.+)\*", r"<i>\1</i>", doc_head)
buff.append(doc_head)
elif isinstance(self.arg, Variant):
buff.append(f"{self.arg.flag_name}:<br/>type: ")
buff.append(
f"{html.escape(self.arg.flag_name, quote=False)}:<br/>type: "
)
buff.append(r"""<span class="dargs-doc-code">""")
buff.append("str")
buff.append(r"""</span>""")
if self.arg.default_tag:
buff.append(", default: ")
buff.append(r"""<span class="dargs-doc-code">""")
buff.append(self.arg.default_tag)
buff.append(html.escape(self.arg.default_tag, quote=False))
buff.append(r"""</span>""")
else:
raise ValueError(f"Unknown type: {type(self.arg)}")

doc_body = html.escape(self.arg.doc.strip())
doc_body = html.escape(self.arg.doc.strip(), quote=False)
if doc_body:
buff.append("<hr/>")
doc_body = re.sub(r"""\n+""", "\n", doc_body)
Expand Down Expand Up @@ -348,7 +353,7 @@ def print_html(self, _level: int = 0, _last_one: bool = True) -> str:
else:
buff.append(r"""<code class="dargs-code">""")
buff.append(
json.dumps(self.data, indent=2)
html.escape(json.dumps(self.data, indent=2), quote=False)
.replace(" ", "&nbsp;")
.replace(
"\n", f"""</code>{linebreak}{indent}<code class="dargs-code">"""
Expand Down
20 changes: 20 additions & 0 deletions tests/test_notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,26 @@

@unittest.skipUnless(ipython_installed, "IPython not installed")
class TestNotebook(unittest.TestCase):
def test_html_escapes_user_content(self) -> None:
"""JSON values, keys, and docs cannot inject executable HTML."""
from dargs.notebook import print_html

dangerous_key = '<img src="x" onerror="alert(1)">'
argument = Argument(
"root",
dict,
[Argument(dangerous_key, str, doc="<script>doc()</script>")],
)
rendered = print_html(
{dangerous_key: "<script>value()</script>"},
argument,
)

self.assertNotIn("<script>", rendered)
self.assertNotIn("<img ", rendered)
self.assertIn("&lt;script&gt;value()", rendered)
self.assertIn("&lt;img src=", rendered)

def test_html_validation(self) -> None:
from dargs.notebook import print_html

Expand Down
Loading