diff --git a/dargs/notebook.py b/dargs/notebook.py index c705dbf..61d851c 100644 --- a/dargs/notebook.py +++ b/dargs/notebook.py @@ -259,11 +259,11 @@ def print_html(self, _level: int = 0, _last_one: bool = True) -> str: buff.append(r"""""") 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('"') @@ -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) # use re to replace ``xx`` to xx doc_head = re.sub( r"``(.*?)``", @@ -285,19 +288,21 @@ def print_html(self, _level: int = 0, _last_one: bool = True) -> str: doc_head = re.sub(r"\*(.+)\*", r"\1", doc_head) buff.append(doc_head) elif isinstance(self.arg, Variant): - buff.append(f"{self.arg.flag_name}:
type: ") + buff.append( + f"{html.escape(self.arg.flag_name, quote=False)}:
type: " + ) buff.append(r"""""") buff.append("str") buff.append(r"""""") if self.arg.default_tag: buff.append(", default: ") buff.append(r"""""") - buff.append(self.arg.default_tag) + buff.append(html.escape(self.arg.default_tag, quote=False)) buff.append(r"""""") 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("
") doc_body = re.sub(r"""\n+""", "\n", doc_body) @@ -348,7 +353,7 @@ def print_html(self, _level: int = 0, _last_one: bool = True) -> str: else: buff.append(r"""""") buff.append( - json.dumps(self.data, indent=2) + html.escape(json.dumps(self.data, indent=2), quote=False) .replace(" ", " ") .replace( "\n", f"""{linebreak}{indent}""" diff --git a/tests/test_notebook.py b/tests/test_notebook.py index 6184197..2113665 100644 --- a/tests/test_notebook.py +++ b/tests/test_notebook.py @@ -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 = '' + argument = Argument( + "root", + dict, + [Argument(dangerous_key, str, doc="")], + ) + rendered = print_html( + {dangerous_key: ""}, + argument, + ) + + self.assertNotIn("