privatize user CSS (fixes #273)

implements issue comment: https://github.com/themotte/rDrama/issues/273#issuecomment-1240543608
This commit is contained in:
justcool393 2023-02-25 02:51:06 -08:00 committed by GitHub
parent d0ba568738
commit fb65cf0416
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 31 additions and 46 deletions

View file

@ -57,8 +57,8 @@ class User(Base):
verifiedcolor = Column(String)
winnings = Column(Integer, default=0, nullable=False)
email = deferred(Column(String))
css = deferred(Column(String))
profilecss = deferred(Column(String))
css = deferred(Column(String(CSS_LENGTH_MAXIMUM)))
profilecss = deferred(Column(String(CSS_LENGTH_MAXIMUM)))
passhash = deferred(Column(String, nullable=False))
post_count = Column(Integer, default=0, nullable=False)
comment_count = Column(Integer, default=0, nullable=False)

View file

@ -62,6 +62,7 @@ COLORS = {'ff66ac','805ad5','62ca56','38a169','80ffff','2a96f3','eb4963','ff0000
SUBMISSION_BODY_LENGTH_MAXIMUM: Final[int] = 20000
COMMENT_BODY_LENGTH_MAXIMUM: Final[int] = 10000
MESSAGE_BODY_LENGTH_MAXIMUM: Final[int] = 10000
CSS_LENGTH_MAXIMUM: Final[int] = 4000
ERROR_MESSAGES = {
400: "That request was bad and you should feel bad",

View file

@ -91,6 +91,7 @@ def inject_constants():
"RENDER_DEPTH_LIMIT":RENDER_DEPTH_LIMIT,
"SORTS_COMMENTS":SORTS_COMMENTS,
"SORTS_POSTS":SORTS_POSTS,
"CSS_LENGTH_MAXIMUM":CSS_LENGTH_MAXIMUM,
}

View file

@ -369,3 +369,12 @@ def filter_emojis_only(title, edit=False, graceful=False):
if len(title) > 1500 and not graceful: abort(400)
else: return title
def validate_css(css:str) -> tuple[bool, str]:
'''
Validates that the provided CSS is allowed. It looks somewhat ugly but
this prevents users from XSSing themselves (not really too much of a
practical concern) or causing styling issues with the rest of the page.
'''
if '</style' in css.lower(): return False, "Invalid CSS"
return True, ""

View file

@ -497,20 +497,21 @@ def settings_images_banner(v):
@app.get("/settings/blocks")
@auth_required
def settings_blockedpage(v):
return render_template("settings_blocks.html", v=v)
@app.get("/settings/css")
@auth_required
def settings_css_get(v):
return render_template("settings_css.html", v=v)
@app.post("/settings/css")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@auth_required
def settings_css(v):
css = request.values.get("css").strip().replace('\\', '').strip()[:4000]
css = sanitize_raw(request.values.get("css", "").replace('\\', ''), allow_newlines=True, length_limit=CSS_LENGTH_MAXIMUM)
ok, err = validate_css(css)
if not ok:
abort(400, err)
v.css = css
g.db.add(v)
g.db.commit()
@ -526,7 +527,10 @@ def settings_profilecss_get(v):
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@auth_required
def settings_profilecss(v):
profilecss = request.values.get("profilecss").strip().replace('\\', '').strip()[:4000]
profilecss = sanitize_raw(request.values.get("profilecss", "").replace('\\', ''), allow_newlines=True, length_limit=CSS_LENGTH_MAXIMUM)
ok, err = validate_css(profilecss)
if not ok:
abort(400, err)
v.profilecss = profilecss
g.db.add(v)
g.db.commit()

View file

@ -380,13 +380,6 @@ def leaderboard(v:User):
return render_template("leaderboard.html", v=v, leaderboards=leaderboards)
@app.get("/@<username>/css")
def get_css(username):
user = get_user(username)
resp=make_response(user.css or "")
resp.headers.add("Content-Type", "text/css")
return resp
@app.get("/@<username>/profilecss")
def get_profilecss(username):
user = get_user(username)

View file

@ -20,7 +20,7 @@
<link rel="stylesheet" href="{{ 'css/main.css' | asset }}">
<link rel="stylesheet" href="{{ ('css/'~v.theme~'.css') | asset }}">
{% if v.css %}
<link rel="stylesheet" href="/@{{v.username}}/css">
<style>{{v.css | safe}}</style>
{% endif %}
{% else %}
<style>:root{--primary:#{{config('DEFAULT_COLOR')}}</style>

View file

@ -19,7 +19,7 @@
<link rel="stylesheet" href="{{ 'css/main.css' | asset }}">
<link rel="stylesheet" href="{{ ('css/'~v.theme~'.css') | asset }}">
{% if v.css %}
<link rel="stylesheet" href="/@{{v.username}}/css">
<style>{{v.css | safe}}</style>
{% endif %}
<style>

View file

@ -14,7 +14,7 @@
<link rel="stylesheet" href="{{ 'css/main.css' | asset }}">
<link rel="stylesheet" href="{{ ('css/'~v.theme~'.css') | asset }}">
{% if v.css %}
<link rel="stylesheet" href="/@{{v.username}}/css">
<style>{{v.css | safe}}</style>
{% endif %}
{% else %}
<style>:root{--primary:#{{config('DEFAULT_COLOR')}}</style>

View file

@ -9,7 +9,7 @@
<link rel="stylesheet" href="{{ 'css/main.css' | asset }}">
<link rel="stylesheet" href="{{ ('css/'~v.theme~'.css') | asset }}">
{% if v.css %}
<link rel="stylesheet" href="/@{{v.username}}/css">
<style>{{v.css | safe}}</style>
{% endif %}
{% else %}
<style>:root{--primary:#{{config('DEFAULT_COLOR')}}</style>

View file

@ -39,7 +39,7 @@
<link rel="stylesheet" href="{{ 'css/main.css' | asset }}">
<link rel="stylesheet" href="{{ ('css/'~v.theme~'.css') | asset }}">
{% if v.css and not request.path.startswith('/settings/css') %}
<link rel="stylesheet" href="/@{{v.username}}/css">
<style>{{v.css | safe}}</style>
{% endif %}
</head>

View file

@ -3,39 +3,27 @@
{% block pagetitle %}Custom CSS - {{SITE_TITLE}}{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-8">
<div class="settings">
<div id="description">
<p class="text-small text-muted">Edit your custom CSS for the site.</p>
<div class="settings-section rounded mb-0">
<div class="body d-lg-flex border-bottom">
<div class="w-lg-100">
<form id="profile-settings" action="/settings/css" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<textarea autocomplete="off" class="form-control rounded" id="bio-text" aria-label="With textarea" placeholder="Custom CSS" rows="50" name="css" form="profile-settings" maxlength="4000">{% if v.css %}{{v.csslazy}}{% endif %}</textarea>
<small>Limit of 4000 characters</small>
<textarea autocomplete="off" class="form-control rounded" id="bio-text" aria-label="With textarea" placeholder="Custom CSS" rows="50" name="css" form="profile-settings" maxlength="{{CSS_LENGTH_MAXIMUM}}">{% if v.css %}{{v.csslazy}}{% endif %}</textarea>
<small>Limit of {{CSS_LENGTH_MAXIMUM}} characters</small>
<div class="d-flex mt-2">
<input autocomplete="off" id="submit-btn" class="btn btn-primary ml-auto" type="submit" value="Save">
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -5,37 +5,26 @@
{% block content %}
<div class="row">
<div class="col col-md-8">
<div class="settings">
<div id="description">
<p class="text-small text-muted">Edit your profile css.</p>
<div class="settings-section rounded mb-0">
<div class="body d-lg-flex border-bottom">
<div class="w-lg-100">
<form id="profile-settings" action="/settings/profilecss" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<textarea autocomplete="off" class="form-control rounded" id="bio-text" aria-label="With textarea" placeholder="Custom profile css" rows="50" name="profilecss" form="profile-settings" maxlength="4000">{% if v.profilecss %}{{v.profilecss}}{% endif %}</textarea>
<small>Limit of 4000 characters</small>
<textarea autocomplete="off" class="form-control rounded" id="bio-text" aria-label="With textarea" placeholder="Custom profile css" rows="50" name="profilecss" form="profile-settings" maxlength="{{CSS_LENGTH_MAXIMUM}}">{% if v.profilecss %}{{v.profilecss}}{% endif %}</textarea>
<small>Limit of {{CSS_LENGTH_MAXIMUM}} characters</small>
<div class="d-flex mt-2">
<input autocomplete="off" class="btn btn-primary ml-auto" type="submit" value="Save">
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -25,7 +25,7 @@
<link rel="stylesheet" href="{{ 'css/main.css' | asset }}">
<link rel="stylesheet" href="{{ ('css/'~v.theme~'.css') | asset }}">
{% if v.css %}
<link rel="stylesheet" href="/@{{v.username}}/css">
<style>{{v.css | safe}}</style>
{% endif %}
{% else %}
<style>:root{--primary:#{{config('DEFAULT_COLOR')}}</style>