This commit is contained in:
admin 2026-11-03 04:48:51 +00:00
parent 2ffc414444
commit bee7213430
3 changed files with 95 additions and 101 deletions

View File

@ -3,10 +3,8 @@ import json
import re
import time
from typing import Optional
import markdown
from sanitizer import sanitize_html_fragment
from html.parser import HTMLParser
from db import get_db
DEFAULT_ROLE_NAME_COLORS = {
@ -14,6 +12,99 @@ DEFAULT_ROLE_NAME_COLORS = {
"moderator": "#8ab4ff",
"user": "#f5f5f7",
}
_ALLOWED_TAGS = {
"b",
"strong",
"i",
"em",
"u",
"p",
"br",
"ul",
"ol",
"li",
"blockquote",
"code",
"pre",
"a",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
}
_ALLOWED_ATTRS = {
"a": {"href", "title", "target", "rel"},
}
_ALLOWED_SCHEMES = ("http://", "https://", "mailto:")
def _escape_attr(value: str) -> str:
return html.escape(value, quote=True)
def sanitize_html_fragment(raw: str) -> str:
class SafeHtmlParser(HTMLParser):
def __init__(self):
super().__init__(convert_charrefs=True)
self.parts = []
def handle_starttag(self, tag, attrs):
tag = tag.lower()
if tag not in _ALLOWED_TAGS:
return
allowed = _ALLOWED_ATTRS.get(tag, set())
clean_attrs = []
for key, value in attrs:
key = (key or "").lower()
value = value or ""
if key not in allowed:
continue
if tag == "a" and key == "href":
lowered = value.strip().lower()
if not lowered.startswith(_ALLOWED_SCHEMES):
continue
clean_attrs.append((key, value))
if tag == "a":
attr_keys = {key for key, _ in clean_attrs}
if "target" not in attr_keys:
clean_attrs.append(("target", "_blank"))
if "rel" not in attr_keys:
clean_attrs.append(("rel", "noopener noreferrer"))
attr_text = "".join(
f' {key}="{_escape_attr(value)}"'
for key, value in clean_attrs
)
self.parts.append(f"<{tag}{attr_text}>")
def handle_endtag(self, tag):
tag = tag.lower()
if tag in _ALLOWED_TAGS and tag not in {"br"}:
self.parts.append(f"</{tag}>")
def handle_data(self, data):
self.parts.append(html.escape(data))
def handle_entityref(self, name):
self.parts.append(f"&{name};")
def handle_charref(self, name):
self.parts.append(f"&#{name};")
parser = SafeHtmlParser()
parser.feed(raw or "")
parser.close()
return "".join(parser.parts)
PROFILE_BIO_MODES = {"txt", "md", "html"}
THEME_MODES = {"dark", "light"}

View File

@ -15,6 +15,7 @@ from .common import (
now_iso,
csrf_valid,
rate_limited,
current_user,
require_user,
require_writable_user,
normalize_username,
@ -30,7 +31,6 @@ from .common import (
normalized_hex_color,
)
def register_profile_routes(app):
@app.route("/api/profile")
def api_get_my_profile():

View File

@ -1,97 +0,0 @@
import html
from html.parser import HTMLParser
_ALLOWED_TAGS = {
"b",
"strong",
"i",
"em",
"u",
"p",
"br",
"ul",
"ol",
"li",
"blockquote",
"code",
"pre",
"a",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
}
_ALLOWED_ATTRS = {
"a": {"href", "title", "target", "rel"},
}
_ALLOWED_SCHEMES = ("http://", "https://", "mailto:")
def _escape_attr(value: str) -> str:
return html.escape(value, quote=True)
def sanitize_html_fragment(raw: str) -> str:
class SafeHtmlParser(HTMLParser):
def __init__(self):
super().__init__(convert_charrefs=True)
self.parts = []
def handle_starttag(self, tag, attrs):
tag = tag.lower()
if tag not in _ALLOWED_TAGS:
return
allowed = _ALLOWED_ATTRS.get(tag, set())
clean_attrs = []
for key, value in attrs:
key = (key or "").lower()
value = value or ""
if key not in allowed:
continue
if tag == "a" and key == "href":
lowered = value.strip().lower()
if not lowered.startswith(_ALLOWED_SCHEMES):
continue
clean_attrs.append((key, value))
if tag == "a":
attr_keys = {key for key, _ in clean_attrs}
if "target" not in attr_keys:
clean_attrs.append(("target", "_blank"))
if "rel" not in attr_keys:
clean_attrs.append(("rel", "noopener noreferrer"))
attr_text = "".join(
f' {key}="{_escape_attr(value)}"'
for key, value in clean_attrs
)
self.parts.append(f"<{tag}{attr_text}>")
def handle_endtag(self, tag):
tag = tag.lower()
if tag in _ALLOWED_TAGS and tag not in {"br"}:
self.parts.append(f"</{tag}>")
def handle_data(self, data):
self.parts.append(html.escape(data))
def handle_entityref(self, name):
self.parts.append(f"&{name};")
def handle_charref(self, name):
self.parts.append(f"&#{name};")
parser = SafeHtmlParser()
parser.feed(raw or "")
parser.close()
return "".join(parser.parts)