Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

safe templates for profile headers #863

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Hush Line implements a series of HTTP security headers to protect our users and
The Content-Security-Policy (CSP) header is a powerful tool used by web applications to mitigate the risk of Cross-Site Scripting (XSS) attacks and other types of code injection attacks. By specifying which content sources are trustworthy, CSP prevents the browser from loading malicious assets. Here's a breakdown of the CSP directive used:

- `default-src 'self';` Only allow content from the site's own origin. This is the default policy for loading resources such as JavaScript, images, CSS, fonts, AJAX requests, frames, HTML5 media, and other data.
- `script-src 'self' https://js.stripe.com https://cdn.jsdelivr.net;`
- `script-src 'self' https://js.stripe.com https://cdn.jsdelivr.net;`
Allow scripts to be loaded from the site’s own origin, Stripe (for payment processing), and jsDelivr (for external libraries like Tesseract.js).
- `img-src 'self' data: https:;` Allow images from the site's origin, inline images using data URIs, and images loaded over HTTPS from any origin.
- `style-src 'self' 'unsafe-inline';` Only the stylesheets from the site's own origin, and dynamically loaded styles for customer instances.
Expand Down
14 changes: 13 additions & 1 deletion hushline/forms.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import html
import re
from typing import Any
from typing import Any, Mapping

from flask_wtf import FlaskForm
from markupsafe import Markup
Expand All @@ -9,6 +9,7 @@
from wtforms.widgets.core import html_params

from hushline.model import MessageStatus
from hushline.safe_template import TemplateError, safe_render_template


class Button:
Expand Down Expand Up @@ -84,3 +85,14 @@ class UpdateMessageStatusForm(FlaskForm):

class DeleteMessageForm(FlaskForm):
submit = SubmitField("Delete", widget=Button())


class ValidTemplate:
def __init__(self, variables: Mapping[str, str]) -> None:
self._variables = variables

def __call__(self, form: Form, field: Field) -> None:
try:
safe_render_template(field.data, self._variables)
except TemplateError as e:
raise ValidationError(str(e))
1 change: 1 addition & 0 deletions hushline/model/username.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class Username(Model):
is_verified: Mapped[bool] = mapped_column(default=False)
show_in_directory: Mapped[bool] = mapped_column(default=False)
bio: Mapped[Optional[str]] = mapped_column(db.Text)
profile_header: Mapped[Optional[str]] = mapped_column(db.Text)

# Extra fields
extra_field_label1: Mapped[Optional[str]]
Expand Down
14 changes: 14 additions & 0 deletions hushline/routes/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
Username,
)
from hushline.routes.forms import MessageForm
from hushline.safe_template import safe_render_template


def register_profile_routes(app: Flask) -> None:
Expand Down Expand Up @@ -54,8 +55,21 @@ def profile(username: str) -> Response | str:
math_problem = f"{num1} + {num2} ="
session["math_answer"] = str(num1 + num2) # Store the answer in session as a string

if uname.profile_header:
profile_header = safe_render_template(
uname.profile_header,
{
"display_name_or_username": uname.display_name or uname.username,
"display_name": uname.display_name,
"username": uname.username,
},
)
else:
profile_header = f"Submit message to {uname.display_name or uname.username}"

return render_template(
"profile.html",
profile_header=profile_header,
form=form,
user=uname.user,
username=uname,
Expand Down
50 changes: 50 additions & 0 deletions hushline/safe_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import re
from typing import Mapping, Optional

VARIABLE_SYNTAX = re.compile("^[a-zA-Z_][a-zA-Z0-9_]*$")
VAR_START = "{{"
VAR_END = "}}"


class TemplateError(Exception):
def __init__(self, details: str) -> None:
super().__init__("There was an error in your template. " + details)
self._details = details


def safe_render_template(template_string: str, variables: Mapping[str, Optional[str]]) -> str:
for name, value in variables.items():
if not VARIABLE_SYNTAX.search(name):
raise TemplateError(f"Variable with invalid syntax: {name}")
if not isinstance(value, str) and value is not None:
# not a template error. this is us making a mistake. don't show to user.
raise ValueError(f"Variable {name} was not a string: {value}")

out = ""

while template_string:
var_start_idx = template_string.find(VAR_START)

# no variables, so just append the remaining string
if var_start_idx < 0:
if template_string.find(VAR_END) >= 0:
raise TemplateError("Invalid syntax. Extra variable substitution braces.")

out += template_string
break

if var_start_idx != 0:
out += template_string[0:var_start_idx]

var_end_idx = template_string.find(VAR_END)
if var_end_idx < 0:
raise TemplateError("Invalid syntax. Variable substitution braces not closed.")

var_name = template_string[var_start_idx + 2 : var_end_idx].strip()
if var_name not in variables:
raise TemplateError(f"Variable not defined: {var_name}")

out += variables[var_name] or "" # to account for None
template_string = template_string[var_end_idx + 2 :]

return out
1 change: 1 addition & 0 deletions hushline/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
UpdateBrandLogoForm,
UpdateBrandPrimaryColorForm,
UpdateDirectoryTextForm,
UpdateProfileHeaderForm,
UserGuidanceAddPromptForm,
UserGuidanceEmergencyExitForm,
UserGuidanceForm,
Expand Down
9 changes: 9 additions & 0 deletions hushline/settings/aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@
handle_new_alias_form,
handle_update_bio,
handle_update_directory_visibility,
handle_update_profile_header,
)
from hushline.settings.forms import (
DirectoryVisibilityForm,
DisplayNameForm,
NewAliasForm,
ProfileForm,
UpdateProfileHeaderForm,
)


Expand Down Expand Up @@ -78,6 +80,7 @@ async def alias(username_id: int) -> Response | str:
directory_visibility_form = DirectoryVisibilityForm(
show_in_directory=alias.show_in_directory
)
update_profile_header_form = UpdateProfileHeaderForm(template=alias.profile_header)

if request.method == "POST":
if "update_bio" in request.form and profile_form.validate_on_submit():
Expand All @@ -89,6 +92,11 @@ async def alias(username_id: int) -> Response | str:
return handle_update_directory_visibility(alias, directory_visibility_form)
elif "update_display_name" in request.form and display_name_form.validate_on_submit():
return handle_display_name_form(alias, display_name_form)
elif (
update_profile_header_form.submit.name in request.form
and update_profile_header_form.validate()
):
return handle_update_profile_header(alias, update_profile_header_form)
else:
current_app.logger.error(
f"Unable to handle form submission on endpoint {request.endpoint!r}, "
Expand All @@ -103,4 +111,5 @@ async def alias(username_id: int) -> Response | str:
display_name_form=display_name_form,
directory_visibility_form=directory_visibility_form,
profile_form=profile_form,
update_profile_header_form=update_profile_header_form,
)
16 changes: 16 additions & 0 deletions hushline/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
NewAliasForm,
PGPKeyForm,
ProfileForm,
UpdateProfileHeaderForm,
)
from hushline.utils import redirect_to_self

Expand Down Expand Up @@ -288,3 +289,18 @@ def handle_email_forwarding_form(
db.session.commit()
flash("👍 SMTP settings updated successfully")
return redirect_to_self()


def handle_update_profile_header(
username: Username,
form: UpdateProfileHeaderForm,
) -> Response:
if form.template.data:
username.profile_header = form.template.data
msg = "👍 Profile header template updated successfully"
else:
username.profile_header = None
msg = "👍 Profile header template reset"
db.session.commit()
flash(msg)
return redirect_to_self()
20 changes: 19 additions & 1 deletion hushline/settings/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from wtforms.validators import Optional as OptionalField

from hushline.db import db
from hushline.forms import Button, CanonicalHTML, ComplexPassword, HexColor
from hushline.forms import Button, CanonicalHTML, ComplexPassword, HexColor, ValidTemplate
from hushline.model import MessageStatus, SMTPEncryption, Username


Expand Down Expand Up @@ -265,3 +265,21 @@ def validate_username(self, field: Field) -> None:
db.exists(Username).where(Username._username == username).select()
):
raise ValidationError(f"Username {username!r} does not exist")


class UpdateProfileHeaderForm(FlaskForm):
template = StringField(
"Custom Profile Header",
validators=[
OptionalField(),
Length(max=500),
ValidTemplate(
{
"display_name_or_username": "x",
"display_name": "x",
"username": "x",
}
),
],
)
submit = SubmitField("Update Profile Header", name="update_profile_header", widget=Button())
9 changes: 9 additions & 0 deletions hushline/settings/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
handle_display_name_form,
handle_update_bio,
handle_update_directory_visibility,
handle_update_profile_header,
)
from hushline.settings.forms import (
DirectoryVisibilityForm,
DisplayNameForm,
ProfileForm,
UpdateProfileHeaderForm,
)


Expand Down Expand Up @@ -52,6 +54,7 @@ async def profile() -> Response | Tuple[str, int]:
for i in range(1, 5)
},
)
update_profile_header_form = UpdateProfileHeaderForm(template=username.profile_header)

status_code = 200
if request.method == "POST":
Expand All @@ -64,6 +67,11 @@ async def profile() -> Response | Tuple[str, int]:
return handle_update_directory_visibility(username, directory_visibility_form)
elif profile_form.submit.name in request.form and profile_form.validate():
return await handle_update_bio(username, profile_form)
elif (
update_profile_header_form.submit.name in request.form
and update_profile_header_form.validate()
):
return handle_update_profile_header(username, update_profile_header_form)
else:
form_error()
status_code = 400
Expand All @@ -85,4 +93,5 @@ async def profile() -> Response | Tuple[str, int]:
directory_visibility_form=directory_visibility_form,
profile_form=profile_form,
business_tier_display_price=business_tier_display_price,
update_profile_header_form=update_profile_header_form,
), status_code
14 changes: 14 additions & 0 deletions hushline/static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,20 @@ pre {
white-space: break-spaces;
}

li code {
padding: 0.25rem;
display: inline-block;
margin: 0 0 0.125rem 0;
}

form li {
margin-bottom: 0.325rem;
}

form ul {
padding-left: 0.75rem;
}

th {
font-weight: normal;
font-family: var(--font-sans-bold);
Expand Down
4 changes: 2 additions & 2 deletions hushline/templates/message.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ <h1>Message</h1>
<div class="message-container">
<div class="message-meta">
<p>
To:
To:
<a href="{{ url_for('profile', username=message.username.username) }}">{{ message.username.display_name or message.username.username }}</a>
</p>
<p class="meta">
Expand All @@ -46,5 +46,5 @@ <h1>Message</h1>
</div>



{% endblock %}
12 changes: 6 additions & 6 deletions hushline/templates/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

{% block title %}
{% if (not require_pgp) or (require_pgp and user.pgp_key) %}
Send a Message to {{ display_name_or_username }}
{{ profile_header }}
{% else %}
Profile: {{ display_name_or_username }}
{% endif %}
Expand All @@ -14,18 +14,18 @@
<h2 class="submit">
{% if current_user_id == user.id %}
{% if user.pgp_key %}
<!-- Authenticated user viewing their own profile with PGP key -->
{# Authenticated user viewing their own profile with PGP key #}
✍️ Note to Self
{% else %}
<!-- Authenticated user viewing their own profile without a PGP key -->
{# Authenticated user viewing their own profile without a PGP key #}
✍️ Note to Self (Add a PGP Key for secure notes)
{% endif %}
{% else %}
{% if (not require_pgp) or (require_pgp and user.pgp_key) %}
<!-- Unauthenticated or other users who meet PGP requirements or don’t require PGP -->
Submit a message to {{ display_name_or_username }}
{# Unauthenticated or other users who meet PGP requirements or don’t require PGP #}
{{ profile_header }}
{% else %}
<!-- Unauthenticated or other users who don’t meet PGP requirements -->
{# Unauthenticated or other users who don’t meet PGP requirements #}
{{ display_name_or_username }}
{% endif %}
{% endif %}
Expand Down
2 changes: 2 additions & 0 deletions hushline/templates/settings/alias.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ <h3>Public User Directory</h3>
</button>
</form>

{% include "settings/update_profile_header.html" %}

<h3>Add Your Bio</h3>
<form
method="POST"
Expand Down
4 changes: 2 additions & 2 deletions hushline/templates/settings/guidance.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ <h2>Settings</h2>
<div class="tab-content">
<h3>User Guidance</h3>
<p>
User Guidance enables you to present information to visitors before they send a
message. This can include cautions about submitting a message using a work
User Guidance enables you to present information to visitors before they send a
message. This can include cautions about submitting a message using a work
device, using Tor Browser, or anything else important for your community.
</p>
<form
Expand Down
2 changes: 2 additions & 0 deletions hushline/templates/settings/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ <h4>Public User Directory</h4>
{{ directory_visibility_form.submit }}
</form>

{% include "settings/update_profile_header.html" %}

<h4>Add Your Bio</h4>
<form
method="POST"
Expand Down
6 changes: 3 additions & 3 deletions hushline/templates/settings/replies.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ <h2>Settings</h2>
{% include "settings/nav.html" %}
<div class="tab-content replies">
<h3>Message Replies</h3>
<p>These are your personal, automated message replies. When you receive a new message, the sender receives a
temporary address where they can view the status of their message. Upon changing the status, the temporary
address will display the most recent updates. While there are pre-defined replies included by default, you
<p>These are your personal, automated message replies. When you receive a new message, the sender receives a
temporary address where they can view the status of their message. Upon changing the status, the temporary
address will display the most recent updates. While there are pre-defined replies included by default, you
can also create custom messages below.</p>
{% for (status, msg_status_text) in status_tuples %}
{% set form = form_maker(status, msg_status_text.markdown if msg_status_text and msg_status_text.markdown else '') %}
Expand Down
Loading
Loading