diff --git a/CHANGES.md b/CHANGES.md
index cabaeb1f..90f815ec 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -2,6 +2,7 @@
## python-markdown2 2.5.6 (not yet released)
+- [pull #739] Fix `header-ids` extra putting sanitized HTML into header ids in safe mode, which allowed attribute injection (#649)
- [pull #732] Fix excessive CPU use in the inline HTML tokenizer on repeated unclosed tag fragments (#707)
- [pull #736] Clarify how to enable file variables in the Python API and command-line help (#269, #365).
- [pull #730] Fix `tables` extra splitting multi-backtick code spans at a leading pipe and merging cells after a code span containing literal backticks.
diff --git a/lib/markdown2.py b/lib/markdown2.py
index 18419ef0..f97e01da 100755
--- a/lib/markdown2.py
+++ b/lib/markdown2.py
@@ -1743,8 +1743,11 @@ def _h_sub(self, match: re.Match[str]) -> str:
n = min(n + demote_headers, 6)
header_id_attr = ""
if "header-ids" in self.extras:
+ # in safe mode, inline HTML is hashed by now. Unhash it so the
+ # sanitized HTML doesn't get swapped back into the id attribute
+ header_text = self._unhash_html_spans(header_group)
header_id = self.header_id_from_text(
- self._unescape_special_chars(header_group),
+ self._unescape_special_chars(header_text),
self.extras["header-ids"].get('prefix'), n)
if header_id:
header_id_attr = ' id="%s"' % header_id
diff --git a/test/tm-cases/header_ids_safe_mode.html b/test/tm-cases/header_ids_safe_mode.html
new file mode 100644
index 00000000..91299c59
--- /dev/null
+++ b/test/tm-cases/header_ids_safe_mode.html
@@ -0,0 +1,7 @@
+
Hello <b>world</b>
+
+<sometext>
+
+
+
+Hi <b x="a" onmouseover="alert(1)">there</b>
diff --git a/test/tm-cases/header_ids_safe_mode.opts b/test/tm-cases/header_ids_safe_mode.opts
new file mode 100644
index 00000000..1d4e5d13
--- /dev/null
+++ b/test/tm-cases/header_ids_safe_mode.opts
@@ -0,0 +1 @@
+{'safe_mode': 'escape', 'extras': ['header-ids']}
\ No newline at end of file
diff --git a/test/tm-cases/header_ids_safe_mode.text b/test/tm-cases/header_ids_safe_mode.text
new file mode 100644
index 00000000..cdf8d539
--- /dev/null
+++ b/test/tm-cases/header_ids_safe_mode.text
@@ -0,0 +1,8 @@
+# Hello world
+
+#
+
+Setext header
+======================
+
+# Hi there
diff --git a/test/tm-cases/header_ids_safe_mode_replace.html b/test/tm-cases/header_ids_safe_mode_replace.html
new file mode 100644
index 00000000..a3c350b9
--- /dev/null
+++ b/test/tm-cases/header_ids_safe_mode_replace.html
@@ -0,0 +1,7 @@
+Hello [HTML_REMOVED]world[HTML_REMOVED]
+
+[HTML_REMOVED]
+
+
+
+Hi [HTML_REMOVED]there[HTML_REMOVED]
diff --git a/test/tm-cases/header_ids_safe_mode_replace.opts b/test/tm-cases/header_ids_safe_mode_replace.opts
new file mode 100644
index 00000000..d6f038f2
--- /dev/null
+++ b/test/tm-cases/header_ids_safe_mode_replace.opts
@@ -0,0 +1 @@
+{'safe_mode': 'replace', 'extras': ['header-ids']}
\ No newline at end of file
diff --git a/test/tm-cases/header_ids_safe_mode_replace.text b/test/tm-cases/header_ids_safe_mode_replace.text
new file mode 100644
index 00000000..cdf8d539
--- /dev/null
+++ b/test/tm-cases/header_ids_safe_mode_replace.text
@@ -0,0 +1,8 @@
+# Hello world
+
+#
+
+Setext header
+======================
+
+# Hi there