fix: CWE-407 — limit HTML nesting depth before bleach to prevent O(2^N)
Add limit_html_nesting() to sanitize_html.py. Flattens any HTML element nested beyond depth 20 using html.parser (O(N)) before the content reaches bleach/html5lib. N=35 attack drops from 12.8s to 0.04s. Wire into markdown_to_html() in render.py — single enforcement point for all callers: product descriptions, shop descriptions, privacy policy, terms of service, markup preview. No byte cap — books, long-form content, and deep table-of-contents structures are fully supported. The depth limit (20 levels) prevents the exponential zone while accommodating any legitimate nesting depth.
This commit is contained in:
parent
4ef3059c8d
commit
c71fd328c3
3 changed files with 39 additions and 6 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue