Use simple string formatting for flash messages.

This addresses an SSTI vulnerability in Flask's `render_template_string`
function, which we were using for rendering flash messages. The
implementation I'd built was too complicated, so I removed its reliance
on Jinja template rendering. Instead, all parts of the flash message
should be keys in the translations file. The `flash` wrapper in
`atst.utils.flash` is just a thin wrapper over our `translate` function.
The `translate` function relies on Python string formatting, which does
not evaluate expressions and so isn't vulnerable to SSTI.
This commit is contained in:
dandds 2020-01-11 15:27:34 -05:00
parent 7de2f440c6
commit 0731b0519c
2 changed files with 138 additions and 118 deletions

View File

@ -1,214 +1,171 @@
from flask import flash, render_template_string
from flask import flash
from atst.utils.localization import translate
MESSAGES = {
"portfolio_deleted": {
"title_template": "Portfolio has been deleted",
"message_template": "Portfolio '{{portfolio_name}}' has been deleted",
"category": "success",
},
"application_created": {
"title_template": translate("flash.application.created.title"),
"message_template": """
{{ "flash.application.created.message" | translate({"application_name": application_name}) }}
""",
"title": "flash.application.created.title",
"message": "flash.application.created.message",
"category": "success",
},
"application_updated": {
"title_template": translate("flash.success"),
"message_template": """
{{ "flash.application.updated" | translate({"application_name": application_name}) }}
""",
"category": "success",
},
"application_deleted": {
"title_template": translate("flash.success"),
"message_template": """
{{ "flash.application.deleted" | translate({"application_name": application_name}) }}
<a href="#">{{ "common.undo" | translate }}</a>
""",
"title": "flash.success",
"message": "flash.application.updated",
"category": "success",
},
"application_environments_name_error": {
"title_template": "",
"message_template": """{{ 'flash.application.env_name_error.message' | translate({ 'name': name }) }}""",
"title": None,
"message": "flash.application.env_name_error.message",
"category": "error",
},
"application_environments_updated": {
"title_template": "Application environments updated",
"message_template": "Application environments have been updated",
"title": "flash.environment.updated.title",
"message": "flash.environment.updated.message",
"category": "success",
},
"application_invite_error": {
"title_template": "Application invitation error",
"message_template": "There was an error processing the invitation for {{ user_name }} from {{ application_name }}",
"title": "flash.application_invite.error.title",
"message": "flash.application_invite.error.message",
"category": "error",
},
"application_invite_resent": {
"title_template": "Application invitation resent",
"message_template": "You have successfully resent the invite for {{ user_name }} from {{ application_name }}",
"title": "flash.application_invite.resent.title",
"message": "flash.application_invite.resent.message",
"category": "success",
},
"application_invite_revoked": {
"title_template": "Application invitation revoked",
"message_template": "You have successfully revoked the invite for {{ user_name }} from {{ application_name }}",
"title": "flash.application_invite.revoked.title",
"message": "flash.application_invite.revoked.message",
"category": "success",
},
"application_member_removed": {
"title_template": "Team member removed from application",
"message_template": "You have successfully deleted {{ user_name }} from {{ application_name }}",
"title": "flash.application_member.removed.title",
"message": "flash.application_member.removed.message",
"category": "success",
},
"application_member_update_error": {
"title_template": "{{ user_name }} could not be updated",
"message_template": "An unexpected problem occurred with your request, please try again. If the problem persists, contact an administrator.",
"title": "flash.application_member.update_error.title",
"message": "flash.application_member.update_error.message",
"category": "error",
},
"application_member_updated": {
"title_template": "Team member updated",
"message_template": "You have successfully updated the permissions for {{ user_name }}",
"title": "flash.application_member.updated.title",
"message": "flash.application_member.updated.message",
"category": "success",
},
"application_name_error": {
"title_template": "",
"message_template": """{{ 'flash.application.name_error.message' | translate({ 'name': name }) }}""",
"title": None,
"message": "flash.application.name_error.message",
"category": "error",
},
"ccpo_user_added": {
"title_template": translate("flash.success"),
"message_template": "You have successfully given {{ user_name }} CCPO permissions.",
"title": "flash.success",
"message": "flash.ccpo_user.added.message",
"category": "success",
},
"ccpo_user_not_found": {
"title_template": translate("ccpo.form.user_not_found_title"),
"message_template": translate("ccpo.form.user_not_found_text"),
"title": "ccpo.form.user_not_found_title",
"message": "ccpo.form.user_not_found_text",
"category": "info",
},
"ccpo_user_removed": {
"title_template": translate("flash.success"),
"message_template": "You have successfully removed {{ user_name }}'s CCPO permissions.",
"title": "flash.success",
"message": "flash.ccpo_user.removed.message",
"category": "success",
},
"environment_added": {
"title_template": translate("flash.success"),
"message_template": """
{{ "flash.environment_added" | translate({ "env_name": environment_name }) }}
""",
"title": "flash.success",
"message": "flash.environment_added",
"category": "success",
},
"environment_deleted": {
"title_template": "{{ environment_name }} deleted",
"message_template": 'The environment "{{ environment_name }}" has been deleted',
"title": "flash.environment.deleted.title",
"message": "flash.environment.deleted.message",
"category": "success",
},
"form_errors": {
"title_template": "There were some errors",
"message_template": "<p>Please see below.</p>",
"title": "flash.form.errors.title",
"message": "flash.form.errors.message",
"category": "error",
},
"insufficient_funds": {
"title_template": "Insufficient Funds",
"message_template": "",
"title": "flash.task_order.insufficient_funds.title",
"message": "",
"category": "warning",
},
"logged_out": {
"title_template": translate("flash.logged_out"),
"message_template": """
You've been logged out.
""",
"title": "flash.logged_out.title",
"message": "flash.logged_out.message",
"category": "info",
},
"login_next": {
"title_template": translate("flash.login_required_title"),
"message_template": translate("flash.login_required_message"),
"title": "flash.login_required_title",
"message": "flash.login_required_message",
"category": "warning",
},
"new_application_member": {
"title_template": """{{ "flash.new_application_member.title" | translate({ "user_name": user_name }) }}""",
"message_template": """
<p>{{ "flash.new_application_member.message" | translate({ "user_name": user_name }) }}</p>
""",
"title": "flash.new_application_member.title",
"message": "flash.new_application_member.message",
"category": "success",
},
"new_portfolio_member": {
"title_template": translate("flash.success"),
"message_template": """
<p>{{ "flash.new_portfolio_member" | translate({ "user_name": user_name }) }}</p>
""",
"title": "flash.success",
"message": "flash.new_portfolio_member",
"category": "success",
},
"portfolio_member_removed": {
"title_template": translate("flash.deleted_member"),
"message_template": """
{{ "flash.delete_member_success" | translate({ "member_name": member_name }) }}
""",
"title": "flash.deleted_member",
"message": "flash.delete_member_success",
"category": "success",
},
"primary_point_of_contact_changed": {
"title_template": translate("flash.new_ppoc_title"),
"message_template": """{{ "flash.new_ppoc_message" | translate({ "ppoc_name": ppoc_name }) }}""",
"title": "flash.new_ppoc_title",
"message": "flash.new_ppoc_message",
"category": "success",
},
"resend_portfolio_invitation": {
"title_template": "Invitation resent",
"message_template": """
<p>Successfully sent a new invitation to {{ user_name }}.</p>
""",
"title": "flash.portfolio_invite.resent.title",
"message": "flash.portfolio_invite.resent.message",
"category": "success",
},
"revoked_portfolio_access": {
"title_template": "Removed portfolio access",
"message_template": """
<p>Portfolio access successfully removed from {{ member_name }}.</p>
""",
"title": "flash.portfolio_member.revoked.title",
"message": "flash.portfolio_member.revoked.message",
"category": "success",
},
"session_expired": {
"title_template": "Session Expired",
"message_template": """
Your session expired due to inactivity. Please log in again to continue.
""",
"title": "flash.session_expired.title",
"message": "flash.session_expired.message",
"category": "error",
},
"task_order_draft": {
"title_template": translate("task_orders.form.draft_alert_title"),
"message_template": translate("task_orders.form.draft_alert_message"),
"title": "task_orders.form.draft_alert_title",
"message": "task_orders.form.draft_alert_message",
"category": "warning",
},
"task_order_number_error": {
"title_template": "",
"message_template": """{{ 'flash.task_order_number_error.message' | translate({ 'to_number': to_number }) }}""",
"title": None,
"message": "flash.task_order_number_error.message",
"category": "error",
},
"task_order_submitted": {
"title_template": "Your Task Order has been uploaded successfully.",
"message_template": """
Your task order form for {{ task_order.portfolio_name }} has been submitted.
""",
"category": "success",
},
"update_portfolio_members": {
"title_template": "Success!",
"message_template": """
<p>You have successfully updated access permissions for members of {{ portfolio.name }}.</p>
""",
"title": "flash.task_order.submitted.title",
"message": "flash.task_order.submitted.message",
"category": "success",
},
"updated_application_team_settings": {
"title_template": translate("flash.success"),
"message_template": """
<p>{{ "flash.updated_application_team_settings" | translate({"application_name": application_name}) }}</p>
""",
"title": "flash.success",
"message": "flash.updated_application_team_settings",
"category": "success",
},
"user_must_complete_profile": {
"title_template": "You must complete your profile",
"message_template": "<p>Before continuing, you must complete your profile</p>",
"title": "flash.user.complete_profile.title",
"message": "flash.user.complete_profile.message",
"category": "info",
},
"user_updated": {
"title_template": "User information updated.",
"message_template": "",
"title": "flash.user.updated.title",
"message": None,
"category": "success",
},
}
@ -216,9 +173,11 @@ MESSAGES = {
def formatted_flash(message_name, **message_args):
config = MESSAGES[message_name]
title = render_template_string(config["title_template"], **message_args)
message = render_template_string(config["message_template"], **message_args)
actions = None
if "actions" in config:
actions = render_template_string(config["actions"], **message_args)
title = translate(config["title"], message_args) if config["title"] else None
message = translate(config["message"], message_args) if config["message"] else None
actions = (
translate(config["actions"], message_args) if config.get("actions") else None
)
flash({"title": title, "message": message, "actions": actions}, config["category"])

View File

@ -118,22 +118,83 @@ flash:
message: 'The application name {name} has already been used in this portfolio. Please enter a unique name.'
env_name_error:
message: 'The environment name {name} has already been used in this application. Please enter a unique name.'
application_invite:
error:
title: Application invitation error
message: There was an error processing the invitation for {user_name} from {application_name}
resent:
title: Application invitation resent
message: You have successfully resent the invite for {user_name} from {application_name}
revoked:
title: Application invitation revoked
message: You have successfully revoked the invite for {user_name} from {application_name}
application_member:
removed:
title: Team member removed from application
message: You have successfully deleted {user_name} from {application_name}
update_error:
title: "{user_name} could not be updated"
message: An unexpected problem occurred with your request, please try again. If the problem persists, contact an administrator.
updated:
title: Team member updated
message: You have successfully updated the permissions for {user_name}
ccpo_user:
added:
message: You have successfully given {user_name} CCPO permissions.
removed:
message: You have successfully removed {user_name}'s CCPO permissions.
delete_member_success: 'You have successfully deleted {member_name} from the portfolio.'
deleted_member: Portfolio member deleted
environment_added: 'The environment "{env_name}" has been added to the application.'
environment_added: 'The environment "{environment_name}" has been added to the application.'
environment:
updated:
title: Application environments updated
message: Application environments have been updated
deleted:
title: "{environment_name} deleted"
message: The environment "{environment_name}" has been deleted
form:
errors:
title: There were some errors
message: Please see below.
login_required_message: After you log in, you will be redirected to your destination page.
login_required_title: Log in required
logged_out:
title: Logged out
message: You've been logged out.
new_portfolio_member: 'You have successfully invited {user_name} to the portfolio.'
new_ppoc_message: 'You have successfully added {ppoc_name} as the primary point of contact. You are no longer the PPoC.'
new_ppoc_title: Primary point of contact updated
portfolio_member:
revoked:
title: Removed portfolio access
message: Portfolio access successfully removed from {member_name}.
portfolio_invite:
resent:
title: Invitation resent
message: Successfully sent a new invitation to {user_name}.
session_expired:
title: Session Expired
message: Your session expired due to inactivity. Please log in again to continue.
success: Success!
task_order_number_error:
message: 'The TO number has already been entered for a JEDI task order #{to_number}. Please double-check the TO number you are entering. If you believe this is in error, please contact support@cloud.mil.'
task_order:
insufficient_funds:
title: Insufficient Funds
submitted:
title: Your Task Order has been uploaded successfully.
message: Your task order form for {task_order.portfolio_name} has been submitted.
new_application_member:
title: "{user_name}'s invitation has been sent"
message: "{user_name}'s access to this Application is pending until they sign in for the first time."
updated_application_team_settings: 'You have updated the {application_name} team settings.'
logged_out: Logged out
user:
complete_profile:
title: You must complete your profile
message: Before continuing, you must complete your profile.
updated:
title: User information updated.
footer:
login: 'Last login:'
forms: