fix xss with service letter contact blocks

service contact blocks contain new lines - and jinja2 normally ignores
newlines (as in it keeps them as new lines) - but we need to turn them
into `<br>` tags so that we can show the formatting that the user has
added. We were previously just doing `{{ block | nl2br | safe }}`. nl2br
turns the new lines into `<br>` tags, and then `safe` tells jinja that
it doesn't need to escape the html.

this causes issues if the user adds `<script>alert(1)</script>` to their
contact block (or some other evil xss hack), where that will get let
through due to the safe flag

To solve this, use `Markup(html='escape')` to sanitise any html, and
then convert new lines to <br>.

bump utils

another xss
This commit is contained in:
Leo Hemsted
2020-01-21 16:50:44 +00:00
parent c57aec8cd5
commit 5bbbdc3cd9
7 changed files with 22 additions and 18 deletions

View File

@@ -8,6 +8,7 @@ from time import monotonic
import ago import ago
import jinja2 import jinja2
from flask import ( from flask import (
Markup,
current_app, current_app,
flash, flash,
g, g,
@@ -26,13 +27,14 @@ from govuk_frontend_jinja.flask_ext import init_govuk_frontend
from itsdangerous import BadSignature from itsdangerous import BadSignature
from notifications_python_client.errors import HTTPError from notifications_python_client.errors import HTTPError
from notifications_utils import formatters, logging, request_helper from notifications_utils import formatters, logging, request_helper
from notifications_utils.formatters import formatted_list from notifications_utils.field import Field
from notifications_utils.recipients import ( from notifications_utils.recipients import (
InvalidPhoneError, InvalidPhoneError,
format_phone_number_human_readable, format_phone_number_human_readable,
validate_phone_number, validate_phone_number,
) )
from notifications_utils.sanitise_text import SanitiseASCII from notifications_utils.sanitise_text import SanitiseASCII
from notifications_utils.take import Take
from notifications_utils.timezones import utc_string_to_aware_gmt_datetime from notifications_utils.timezones import utc_string_to_aware_gmt_datetime
from werkzeug.exceptions import HTTPException as WerkzeugHTTPException from werkzeug.exceptions import HTTPException as WerkzeugHTTPException
from werkzeug.exceptions import abort from werkzeug.exceptions import abort
@@ -467,7 +469,14 @@ def format_notification_status_as_url(status, notification_type):
def nl2br(value): def nl2br(value):
return formatters.nl2br(value) if value else '' if value:
return Markup(Take(Field(
value,
html='escape',
)).then(
formatters.nl2br
))
return ''
@login_manager.user_loader @login_manager.user_loader
@@ -740,7 +749,7 @@ def add_template_filters(application):
format_notification_status_as_time, format_notification_status_as_time,
format_notification_status_as_field_status, format_notification_status_as_field_status,
format_notification_status_as_url, format_notification_status_as_url,
formatted_list, formatters.formatted_list,
nl2br, nl2br,
format_phone_number_human_readable, format_phone_number_human_readable,
format_thousands, format_thousands,

View File

@@ -1,6 +1,7 @@
import re import re
from notifications_utils.field import Field from notifications_utils.field import Field
from notifications_utils.formatters import formatted_list
from notifications_utils.recipients import ( from notifications_utils.recipients import (
InvalidEmailError, InvalidEmailError,
validate_email_address, validate_email_address,
@@ -9,7 +10,6 @@ from notifications_utils.sanitise_text import SanitiseSMS
from wtforms import ValidationError from wtforms import ValidationError
from wtforms.validators import Email from wtforms.validators import Email
from app import formatted_list
from app.main._blacklisted_passwords import blacklisted_passwords from app.main._blacklisted_passwords import blacklisted_passwords
from app.utils import Spreadsheet, is_gov_user from app.utils import Spreadsheet, is_gov_user

View File

@@ -1,7 +1,4 @@
from flask import Markup, abort, current_app from flask import abort, current_app
from notifications_utils.field import Field
from notifications_utils.formatters import nl2br
from notifications_utils.take import Take
from werkzeug.utils import cached_property from werkzeug.utils import cached_property
from app.models import JSONModel from app.models import JSONModel
@@ -358,13 +355,11 @@ class Service(JSONModel):
@property @property
def default_letter_contact_block_html(self): def default_letter_contact_block_html(self):
# import in the function to prevent cyclical imports
from app import nl2br
if self.default_letter_contact_block: if self.default_letter_contact_block:
return Markup(Take(Field( return nl2br(self.default_letter_contact_block['contact_block'])
self.default_letter_contact_block['contact_block'],
html='escape',
)).then(
nl2br
))
return '' return ''
def edit_letter_contact_block(self, id, contact_block, is_default): def edit_letter_contact_block(self, id, contact_block, is_default):

View File

@@ -136,7 +136,7 @@
{% call settings_row(if_has_permission='sms') %} {% call settings_row(if_has_permission='sms') %}
{{ text_field('Text message senders') }} {{ text_field('Text message senders') }}
{% call field(status='default' if current_service.default_sms_sender == "None" else '') %} {% call field(status='default' if current_service.default_sms_sender == "None" else '') %}
{{ current_service.default_sms_sender | string | nl2br | safe if current_service.default_sms_sender else 'None'}} {{ current_service.default_sms_sender | string | nl2br if current_service.default_sms_sender else 'None'}}
{% if current_service.count_sms_senders > 1 %} {% if current_service.count_sms_senders > 1 %}
<div class="hint"> <div class="hint">
{{ '…and %d more' | format(current_service.count_sms_senders - 1) }} {{ '…and %d more' | format(current_service.count_sms_senders - 1) }}

View File

@@ -32,7 +32,7 @@
{% for item in letter_contact_details %} {% for item in letter_contact_details %}
<div class="user-list-item"> <div class="user-list-item">
<p> <p>
{{ item.contact_block | nl2br | safe }} {{ item.contact_block | nl2br }}
</p> </p>
<p class="hint"> <p class="hint">
{%- if item.is_default -%} {%- if item.is_default -%}

View File

@@ -23,5 +23,5 @@ awscli-cwlogs>=1.4,<1.5
# Putting upgrade on hold due to v1.0.0 using sha512 instead of sha1 by default # Putting upgrade on hold due to v1.0.0 using sha512 instead of sha1 by default
itsdangerous==0.24 # pyup: <1.0.0 itsdangerous==0.24 # pyup: <1.0.0
git+https://github.com/alphagov/notifications-utils.git@36.4.0#egg=notifications-utils==36.4.0 git+https://github.com/alphagov/notifications-utils.git@36.4.1#egg=notifications-utils==36.4.1
git+https://github.com/alphagov/govuk-frontend-jinja.git@v0.5.1-alpha#egg=govuk-frontend-jinja==0.5.1-alpha git+https://github.com/alphagov/govuk-frontend-jinja.git@v0.5.1-alpha#egg=govuk-frontend-jinja==0.5.1-alpha

View File

@@ -25,7 +25,7 @@ awscli-cwlogs>=1.4,<1.5
# Putting upgrade on hold due to v1.0.0 using sha512 instead of sha1 by default # Putting upgrade on hold due to v1.0.0 using sha512 instead of sha1 by default
itsdangerous==0.24 # pyup: <1.0.0 itsdangerous==0.24 # pyup: <1.0.0
git+https://github.com/alphagov/notifications-utils.git@36.4.0#egg=notifications-utils==36.4.0 git+https://github.com/alphagov/notifications-utils.git@36.4.1#egg=notifications-utils==36.4.1
git+https://github.com/alphagov/govuk-frontend-jinja.git@v0.5.1-alpha#egg=govuk-frontend-jinja==0.5.1-alpha git+https://github.com/alphagov/govuk-frontend-jinja.git@v0.5.1-alpha#egg=govuk-frontend-jinja==0.5.1-alpha
## The following requirements were added by pip freeze: ## The following requirements were added by pip freeze: