From 386db76c1092ca115e40dd0503c26e475f5e753b Mon Sep 17 00:00:00 2001 From: Aevann1 Date: Wed, 25 May 2022 20:29:22 +0200 Subject: [PATCH] crgd is a king --- files/classes/submission.py | 6 +++--- files/helpers/const.py | 12 ++++++++---- files/helpers/sanitize.py | 14 +++++--------- files/routes/admin.py | 2 ++ files/routes/errors.py | 2 +- files/routes/login.py | 8 ++++---- files/routes/posts.py | 13 ++++++++----- files/routes/settings.py | 5 ++--- files/routes/subs.py | 5 ++--- 9 files changed, 35 insertions(+), 32 deletions(-) diff --git a/files/classes/submission.py b/files/classes/submission.py index 63834b7c7..87f73b8f3 100644 --- a/files/classes/submission.py +++ b/files/classes/submission.py @@ -472,17 +472,17 @@ class Submission(Base): @property @lazy def is_video(self): - return self.url and any((self.url.lower().endswith(x) for x in ('.mp4','.webm','.mov'))) and embed_fullmatch_regex.fullmatch(self.url) + return self.url and any((self.url.lower().endswith(x) for x in ('.mp4','.webm','.mov'))) and is_safe_url(self.url) @property @lazy def is_audio(self): - return self.url and any((self.url.lower().endswith(x) for x in ('.mp3','.wav','.ogg','.aac','.m4a','.flac'))) and embed_fullmatch_regex.fullmatch(self.url) + return self.url and any((self.url.lower().endswith(x) for x in ('.mp3','.wav','.ogg','.aac','.m4a','.flac'))) and is_safe_url(self.url) @property @lazy def is_image(self): - if self.url and (self.url.lower().endswith('.webp') or self.url.lower().endswith('.jpg') or self.url.lower().endswith('.png') or self.url.lower().endswith('.gif') or self.url.lower().endswith('.jpeg') or self.url.lower().endswith('?maxwidth=9999') or self.url.lower().endswith('&fidelity=high')) and (self.url.startswith('/') or self.url.startswith(f'{SITE_FULL}/') or embed_fullmatch_regex.fullmatch(self.url)): + if self.url and (self.url.lower().endswith('.webp') or self.url.lower().endswith('.jpg') or self.url.lower().endswith('.png') or self.url.lower().endswith('.gif') or self.url.lower().endswith('.jpeg') or self.url.lower().endswith('?maxwidth=9999') or self.url.lower().endswith('&fidelity=high')) and is_safe_url(self.url): return True return False diff --git a/files/helpers/const.py b/files/helpers/const.py index e40141e94..d1646580f 100644 --- a/files/helpers/const.py +++ b/files/helpers/const.py @@ -6,6 +6,7 @@ from files.__main__ import db_session from files.classes.sub import Sub from files.classes.marsey import Marsey from flask import request +import tldextract SITE = environ.get("DOMAIN", '').strip() SITE_NAME = environ.get("SITE_NAME", '').strip() @@ -875,7 +876,7 @@ proxies = {"http":"http://127.0.0.1:18080","https":"http://127.0.0.1:18080"} blackjack = environ.get("BLACKJACK", "").strip() -approved_embed_hosts = [ +approved_embed_hosts = { SITE, 'rdrama.net', 'pcmemes.net', @@ -935,14 +936,17 @@ approved_embed_hosts = [ 'typekit.net', 'postimg.cc', 'archive.org' - ] + } + + +def is_safe_url(url): + return '\\' not in url and (url.startswith('/') or tldextract.extract(url).registered_domain in approved_embed_hosts) + hosts = "|".join(approved_embed_hosts).replace('.','\.') image_check_regex = re.compile(f'!\[\]\(((?!(https:\/\/([a-z0-9-]+\.)*({hosts})\/|\/)).*?)\)', flags=re.A) -embed_fullmatch_regex = re.compile(f'https:\/\/([a-z0-9-]+\.)*({hosts})\/[\w:~,()\-.#&\/=?@%;+]*', flags=re.A) - video_sub_regex = re.compile(f'(

[^<]*)(https:\/\/([a-z0-9-]+\.)*({hosts})\/[\w:~,()\-.#&\/=?@%;+]*?\.(mp4|webm|mov))', flags=re.A) audio_sub_regex = re.compile(f'(

[^<]*)(https:\/\/([a-z0-9-]+\.)*({hosts})\/[\w:~,()\-.#&\/=?@%;+]*?\.(mp3|wav|ogg|aac|m4a|flac))', flags=re.A) diff --git a/files/helpers/sanitize.py b/files/helpers/sanitize.py index 18ebca785..956a230da 100644 --- a/files/helpers/sanitize.py +++ b/files/helpers/sanitize.py @@ -53,16 +53,13 @@ def allowed_attributes(tag, name, value): return False if tag == 'a': - if name == 'href': return True + if name == 'href' and '\\' not in value: return True if name == 'rel' and value == 'nofollow noopener noreferrer': return True if name == 'target' and value == '_blank': return True return False if tag == 'img': - if name in ['src','data-src']: - if value.startswith('/') or value.startswith(f'{SITE_FULL}/') or embed_fullmatch_regex.fullmatch(value): return True - else: return False - + if name in ['src','data-src']: return is_safe_url(value) if name == 'loading' and value == 'lazy': return True if name == 'data-bs-toggle' and value == 'tooltip': return True if name in ['g','b'] and not value: return True @@ -81,13 +78,12 @@ def allowed_attributes(tag, name, value): return False if tag == 'source': - if name == 'src' and embed_fullmatch_regex.fullmatch(value): return True - return False + if name == 'src': return is_safe_url(value) if tag == 'audio': + if name == 'src': return is_safe_url(value) if name == 'controls' and value == '': return True if name == 'preload' and value == 'none': return True - if name == 'src' and embed_fullmatch_regex.fullmatch(value): return True return False if tag == 'p': @@ -349,7 +345,7 @@ def sanitize(sanitized, alert=False, comment=False, edit=False): def allowed_attributes_emojis(tag, name, value): if tag == 'img': - if name == 'src' and value.startswith('/'): return True + if name == 'src' and value.startswith('/') and '\\' not in value: return True if name == 'loading' and value == 'lazy': return True if name == 'data-bs-toggle' and value == 'tooltip': return True if name == 'g' and not value: return True diff --git a/files/routes/admin.py b/files/routes/admin.py index 53be8a068..fdc265b1f 100644 --- a/files/routes/admin.py +++ b/files/routes/admin.py @@ -660,6 +660,8 @@ def badge_grant_post(v): if desc: new_badge.description = desc url = request.values.get("url") + if '\\' in url: abort(400) + if url: new_badge.url = url g.db.add(new_badge) diff --git a/files/routes/errors.py b/files/routes/errors.py index aaa18ccf1..f865d442e 100644 --- a/files/routes/errors.py +++ b/files/routes/errors.py @@ -72,5 +72,5 @@ def allow_nsfw(): redir = request.values.get("redir") if redir: if redir.startswith(f'{SITE_FULL}/'): return redirect(redir) - if redir.startswith('/'): return redirect(f'{SITE_FULL}{redir}') + if redir.startswith('/') and '\\' not in redir: return redirect(f'{SITE_FULL}{redir}') return redirect('/') \ No newline at end of file diff --git a/files/routes/login.py b/files/routes/login.py index 43f53b2c6..9e0b95a30 100644 --- a/files/routes/login.py +++ b/files/routes/login.py @@ -11,11 +11,11 @@ def login_get(v): redir = request.values.get("redirect") if redir: redir = redir.replace("/logged_out", "").strip() - if not redir.startswith(f'{SITE_FULL}/') and not redir.startswith('/'): redir = None + if not redir.startswith(f'{SITE_FULL}/') and not (redir.startswith('/') and '\\' not in redir): redir = None if v and redir: if redir.startswith(f'{SITE_FULL}/'): return redirect(redir) - elif redir.startswith('/'): return redirect(f'{SITE_FULL}{redir}') + elif redir.startswith('/') and '\\' not in redir: return redirect(f'{SITE_FULL}{redir}') return render_template("login.html", failed=False, redirect=redir) @@ -150,11 +150,11 @@ def login_post(): redir = request.values.get("redirect") if redir: redir = redir.replace("/logged_out", "").strip() - if not redir.startswith(f'{SITE_FULL}/') and not redir.startswith('/'): redir = '/' + if not redir.startswith(f'{SITE_FULL}/') and not (redir.startswith('/') and '\\' not in redir): redir = '/' if redir: if redir.startswith(f'{SITE_FULL}/'): return redirect(redir) - if redir.startswith('/'): return redirect(f'{SITE_FULL}{redir}') + if redir.startswith('/') and '\\' not in redir: return redirect(f'{SITE_FULL}{redir}') return redirect('/') @app.get("/me") diff --git a/files/routes/posts.py b/files/routes/posts.py index d4f8d84e5..1448e7473 100644 --- a/files/routes/posts.py +++ b/files/routes/posts.py @@ -585,7 +585,7 @@ def thumbnail_thread(pid): return f"https://{fragment_url.split('https://')[1]}" elif fragment_url.startswith('//'): return f"https:{fragment_url}" - elif fragment_url.startswith('/'): + elif fragment_url.startswith('/') and '\\' not in url: parsed_url = urlparse(post_url) return f"https://{parsed_url.netloc}{fragment_url}" else: @@ -601,7 +601,8 @@ def thumbnail_thread(pid): fetch_url = post.url - if fetch_url.startswith('/'): fetch_url = f"{SITE_FULL}{fetch_url}" + if fetch_url.startswith('/') and '\\' not in url: + fetch_url = f"{SITE_FULL}{fetch_url}" headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36"} @@ -854,9 +855,11 @@ def api_is_repost(): @auth_required def submit_post(v, sub=None): - title = request.values.get("title", "").strip()[:500].replace('‎','') - url = request.values.get("url", "").strip() + + if '\\' in url: abort(400) + + title = request.values.get("title", "").strip()[:500].replace('‎','') body = request.values.get("body", "").strip().replace('‎','') @@ -1516,7 +1519,7 @@ def api_pin_post(post_id, v): def get_post_title(v): url = request.values.get("url") - if not url: abort(400) + if not url or '\\' in url: abort(400) try: x = requests.get(url, headers=titleheaders, timeout=5, proxies=proxies) except: abort(400) diff --git a/files/routes/settings.py b/files/routes/settings.py index 927e5afe7..7bc8763a7 100644 --- a/files/routes/settings.py +++ b/files/routes/settings.py @@ -641,9 +641,8 @@ def settings_profilecss(v): urls = list(css_regex.finditer(profilecss)) + list(css_regex2.finditer(profilecss)) for i in urls: url = i.group(1) - if url.startswith('/'): continue - domain = tldextract.extract(url).registered_domain - if domain not in approved_embed_hosts: + 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) diff --git a/files/routes/subs.py b/files/routes/subs.py index 87b5fd36f..b65a85498 100644 --- a/files/routes/subs.py +++ b/files/routes/subs.py @@ -341,9 +341,8 @@ def post_sub_css(v, sub): urls = list(css_regex.finditer(css)) + list(css_regex2.finditer(css)) for i in urls: url = i.group(1) - if url.startswith('/'): continue - domain = tldextract.extract(url).registered_domain - if domain not in approved_embed_hosts: + 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)