From 59344a28cdcd463955ae3b98b3c71b45f961bbcd Mon Sep 17 00:00:00 2001 From: TLSM Date: Fri, 5 Aug 2022 12:32:56 -0400 Subject: [PATCH 1/2] Fix safe_url bypass for profilecss external embeds. --- files/helpers/const.py | 2 +- files/helpers/regex.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/files/helpers/const.py b/files/helpers/const.py index 401245143..296f021f1 100644 --- a/files/helpers/const.py +++ b/files/helpers/const.py @@ -998,7 +998,7 @@ approved_embed_hosts = { def is_site_url(url): - return url and '\\' not in url and (url.startswith('/') or url.startswith(f'{SITE_FULL}/')) + return url and '\\' not in url and ((url.startswith('/') and not url.startswith('//')) or url.startswith(f'{SITE_FULL}/')) def is_safe_url(url): return is_site_url(url) or tldextract.extract(url).registered_domain in approved_embed_hosts diff --git a/files/helpers/regex.py b/files/helpers/regex.py index 0ba20a247..437b8b5ca 100644 --- a/files/helpers/regex.py +++ b/files/helpers/regex.py @@ -80,7 +80,7 @@ image_regex = re.compile("(^|\s)(https:\/\/[\w\-.#&/=\?@%;+,:]{5,250}(\.png|\.jp link_fix_regex = re.compile("(\[.*?\]\()(?!http|/)(.*?\))", flags=re.A) -css_regex = re.compile('https?:\/\/[\w:~,()\-.#&\/=?@%;+]*', flags=re.I|re.A) +css_regex = re.compile('(https?:)?\/\/[\w:~,()\-.#&\/=?@%;+]*', flags=re.I|re.A) procoins_li = (0,2500,5000,10000,25000,50000,125000,250000) From 8b241a765a272d7002ea2dee9b3b715e96795ba2 Mon Sep 17 00:00:00 2001 From: TLSM Date: Fri, 5 Aug 2022 13:09:41 -0400 Subject: [PATCH 2/2] Check URI approved embed in all CSS contexts. --- files/helpers/regex.py | 2 +- files/helpers/sanitize.py | 12 ++++++++++++ files/routes/settings.py | 11 +++-------- files/routes/subs.py | 20 ++++++++------------ 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/files/helpers/regex.py b/files/helpers/regex.py index 437b8b5ca..039220e3d 100644 --- a/files/helpers/regex.py +++ b/files/helpers/regex.py @@ -80,7 +80,7 @@ image_regex = re.compile("(^|\s)(https:\/\/[\w\-.#&/=\?@%;+,:]{5,250}(\.png|\.jp link_fix_regex = re.compile("(\[.*?\]\()(?!http|/)(.*?\))", flags=re.A) -css_regex = re.compile('(https?:)?\/\/[\w:~,()\-.#&\/=?@%;+]*', flags=re.I|re.A) +css_url_regex = re.compile('url\(\s*[\'"]?(.*?)[\'"]?\s*\)', flags=re.I|re.A) procoins_li = (0,2500,5000,10000,25000,50000,125000,250000) diff --git a/files/helpers/sanitize.py b/files/helpers/sanitize.py index 9f21b11f8..6a16f42d6 100644 --- a/files/helpers/sanitize.py +++ b/files/helpers/sanitize.py @@ -458,3 +458,15 @@ def normalize_url(url): url = giphy_regex.sub(r'\1.webp', url) return url + +def validate_css(css): + if '@import' in css: + return False, "@import statements not allowed." + + for i in css_url_regex.finditer(css): + url = i.group(1) + if not is_safe_url(url): + domain = tldextract.extract(url).registered_domain + return False, f"The domain '{domain}' is not allowed, please use one of these domains\n\n{approved_embed_hosts}." + + return True, "" diff --git a/files/routes/settings.py b/files/routes/settings.py index 5f0fb74fb..b1f747e90 100644 --- a/files/routes/settings.py +++ b/files/routes/settings.py @@ -599,14 +599,9 @@ def settings_profilecss_get(v): def settings_profilecss(v): profilecss = request.values.get("profilecss").strip().replace('\\', '').strip()[:4000] - - for i in css_regex.finditer(profilecss): - url = i.group(0) - if not is_safe_url(url): - domain = tldextract.extract(url).registered_domain - error = f"The domain '{domain}' is not allowed, please use one of these domains\n\n{approved_embed_hosts}." - return render_template("settings_profilecss.html", error=error, v=v) - + valid, error = validate_css(profilecss) + if not valid: + return render_template("settings_profilecss.html", error=error, v=v) v.profilecss = profilecss g.db.add(v) diff --git a/files/routes/subs.py b/files/routes/subs.py index e3dc8f85c..cb368bea7 100644 --- a/files/routes/subs.py +++ b/files/routes/subs.py @@ -352,21 +352,17 @@ def post_sub_sidebar(v, sub): @is_not_permabanned def post_sub_css(v, sub): sub = g.db.query(Sub).filter_by(name=sub.strip().lower()).one_or_none() - if not sub: abort(404) - - if not v.mods(sub.name): abort(403) - css = request.values.get('css', '').strip() + if not sub: + abort(404) + if not v.mods(sub.name): + abort(403) - for i in css_regex.finditer(css): - url = i.group(0) - if not is_safe_url(url): - domain = tldextract.extract(url).registered_domain - error = f"The domain '{domain}' is not allowed, please use one of these domains\n\n{approved_embed_hosts}." - return render_template('sub/settings.html', v=v, sidebar=sub.sidebar, sub=sub, error=error) - - + valid, error = validate_css(css) + if not valid: + return render_template('sub/settings.html', + v=v, sidebar=sub.sidebar, sub=sub, error=error) sub.css = css g.db.add(sub)