Defense-in-depth: neutralize raw HTML in md_to_html.py itself
The previous fix escaped AD data at generation time in both scripts, but the converter still blindly trusted its input -- a report from before that fix, from a hand edit, or from a third-party tool would still render live HTML unmodified. Normalize the whole Markdown source before parsing: unescape any existing entities, then re-escape &, <, > uniformly. The round trip keeps already-escaped (freshly generated) reports single-escaped instead of doubling up, while raw/legacy unescaped HTML gets neutralized for the first time. Neither script intentionally emits raw HTML, so this is safe across all normal report content. Verified: a freshly-escaped report stays single-escaped (AT&T reads as AT&T, not AT&amp;T), a hand-written report with a live <script> tag gets neutralized, and a full report with badges/TOC/ timestamps/tables still renders identically to before.
This commit is contained in:
+20
-1
@@ -392,11 +392,30 @@ def find_latest_report(reports_dir):
|
||||
return candidates[-1]
|
||||
|
||||
|
||||
def neutralize_raw_html(md_text):
|
||||
"""Defense-in-depth: strip any live HTML out of the Markdown source
|
||||
before parsing, regardless of whether the report generator already
|
||||
escaped it.
|
||||
|
||||
The generator scripts (ad_audit.py / Invoke-ADAudit.ps1) escape AD data
|
||||
before writing it into table cells, but this converter shouldn't have to
|
||||
trust that: a report could have been generated by an older version, by
|
||||
a third-party tool, or hand-edited. Neither of our reports intentionally
|
||||
emits raw HTML, so it's safe to normalize the whole source first --
|
||||
unescape any existing entities, then re-escape uniformly. That round
|
||||
trip keeps already-escaped content single-escaped instead of doubling
|
||||
up (which naive blanket-escaping would do to freshly generated reports).
|
||||
"""
|
||||
unescaped = html.unescape(md_text)
|
||||
return unescaped.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
|
||||
def convert(md_path, out_path):
|
||||
with open(md_path, "r", encoding="utf-8") as f:
|
||||
md_text = f.read()
|
||||
|
||||
body_html = markdown.markdown(md_text, extensions=["tables", "fenced_code", "toc"])
|
||||
safe_md_text = neutralize_raw_html(md_text)
|
||||
body_html = markdown.markdown(safe_md_text, extensions=["tables", "fenced_code", "toc"])
|
||||
body_html = badge_findings_table(body_html)
|
||||
body_html = wrap_timestamps(body_html)
|
||||
toc_html = build_toc(body_html)
|
||||
|
||||
Reference in New Issue
Block a user