diff --git a/make_post_sell/lib/render.py b/make_post_sell/lib/render.py index 47d54e0..3688555 100644 --- a/make_post_sell/lib/render.py +++ b/make_post_sell/lib/render.py @@ -1,5 +1,6 @@ from .sanitize_html import ( default_cleaner, + limit_html_nesting, markdown_to_raw_html, clean_raw_html, ) @@ -60,6 +61,7 @@ def add_shop_theme_classes(html, shop): def markdown_to_html(data, shop=None): raw_html = markdown_to_raw_html(data) + raw_html = limit_html_nesting(raw_html) if shop: cleaner = make_cleaner_from_shop(shop) else: diff --git a/make_post_sell/lib/sanitize_html.py b/make_post_sell/lib/sanitize_html.py index 0975533..7b7ffc9 100644 --- a/make_post_sell/lib/sanitize_html.py +++ b/make_post_sell/lib/sanitize_html.py @@ -12,7 +12,7 @@ from bleach_allowlist import markdown_tags, markdown_attrs, all_styles # We implement our own CSS validation in protect_links() for security -from bs4 import BeautifulSoup +from bs4 import BeautifulSoup, Tag import miniuri @@ -105,6 +105,41 @@ def default_cleaner(tag_acl=None): return cleaner +def limit_html_nesting(html, max_depth=20): + """ + Flatten HTML elements nested deeper than max_depth. + + bleach/html5lib has O(2^N) complexity for deeply nested or misnested + elements (CWE-407). N=30 → 1.0s, N=35 → 12.8s measured in the wild. + This runs on the raw markdown output (before bleach) using html.parser + which is O(N) — safe to call first. + + max_depth=20 accommodates books with deeply nested lists, blockquotes, + and table-of-contents structures while keeping N well below the + exponential zone. + """ + soup = BeautifulSoup(html, "html.parser") + + to_unwrap = [] + + def _collect(node, depth): + for child in list(node.children): + if not isinstance(child, Tag): + continue + if depth >= max_depth: + to_unwrap.append(child) + _collect(child, depth + 1) + + _collect(soup, 0) + + # Unwrap deepest first so parent references remain valid + for tag in reversed(to_unwrap): + if tag.parent is not None: + tag.unwrap() + + return str(soup) + + def markdown_to_raw_html(data, extra_extensions=None): """Accepts a markdown string, returns raw unsanitized HTML""" extensions = [ diff --git a/make_post_sell/views/misc.py b/make_post_sell/views/misc.py index d5d13ff..942dfbd 100644 --- a/make_post_sell/views/misc.py +++ b/make_post_sell/views/misc.py @@ -83,11 +83,7 @@ def ask_for_on_demand_tls(request): ) def markup_editor_preview(request): """AJAJ: Accept Markup data param, return HTML""" - data = request.params.get("data", "") - if len(data) > 100_000: - request.response.status = 400 - return "input too large" - return markdown_to_html(data, request.shop) + return markdown_to_html(request.params.get("data", ""), request.shop) # currently only supports markdown, but eventually we could support others. # try: # return markdown_to_html(request.params["data"], request.shop)