more input checking

This commit is contained in:
Kenneth Kehl
2025-06-26 10:35:46 -07:00
parent 3e93d6c9c6
commit 58a8b51f59
16 changed files with 214 additions and 86 deletions

View File

@@ -1,8 +1,7 @@
import re
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
import dateutil import dateutil
from flask import Blueprint, abort, current_app, jsonify, request from flask import Blueprint, current_app, jsonify, request
from app import db from app import db
from app.aws.s3 import ( from app.aws.s3 import (
@@ -37,7 +36,7 @@ from app.schemas import (
notification_with_template_schema, notification_with_template_schema,
notifications_filter_schema, notifications_filter_schema,
) )
from app.utils import midnight_n_days_ago, pagination_links from app.utils import check_suspicious_id, midnight_n_days_ago, pagination_links
job_blueprint = Blueprint("job", __name__, url_prefix="/service/<uuid:service_id>/job") job_blueprint = Blueprint("job", __name__, url_prefix="/service/<uuid:service_id>/job")
@@ -45,55 +44,6 @@ job_blueprint = Blueprint("job", __name__, url_prefix="/service/<uuid:service_id
register_errors(job_blueprint) register_errors(job_blueprint)
def is_suspicious_input(input_str):
if not isinstance(input_str, str):
return False
pattern = re.compile(
r"""
(?i) # case insensite
\b # word boundary
( # start of group for SQL keywords
OR # match SQL keyword OR
|AND
|UNION
|SELECT
|DROP
|INSERT
|UPDATE
|DELETE
|EXEC
|TRUNCATE
|CREATE
|ALTER
|-- # match SQL single-line comment
|/\* # match SQL multi-line comment
|\bpg_sleep\b # Match PostgreSQL 'pg_sleep' function
|\bsleep\b # Match SQL Server 'sleep' function
) # End SQL keywords and function group
| # OR operator to include an alternate pattern
[';]{2,} # Match two or more consecutive single quotes or semi-colons
""",
re.VERBOSE,
)
return bool(re.search(pattern, input_str))
def is_valid_id(id):
if not isinstance(id, str):
return True
return bool(re.match(r"^[a-zA-Z0-9_-]{1,50}$", id))
def check_suspicious_id(*args):
for id in args:
if not is_valid_id(id):
abort(403)
if is_suspicious_input(id):
abort(403)
@job_blueprint.route("/<job_id>", methods=["GET"]) @job_blueprint.route("/<job_id>", methods=["GET"])
def get_job_by_service_and_job_id(service_id, job_id): def get_job_by_service_and_job_id(service_id, job_id):
check_suspicious_id(service_id, job_id) check_suspicious_id(service_id, job_id)

View File

@@ -23,6 +23,7 @@ from app.schemas import (
) )
from app.service.utils import service_allowed_to_send_to from app.service.utils import service_allowed_to_send_to
from app.utils import ( from app.utils import (
check_suspicious_id,
get_public_notify_type_text, get_public_notify_type_text,
get_template_instance, get_template_instance,
pagination_links, pagination_links,
@@ -36,6 +37,7 @@ register_errors(notifications)
@notifications.route("/notifications/<uuid:notification_id>", methods=["GET"]) @notifications.route("/notifications/<uuid:notification_id>", methods=["GET"])
def get_notification_by_id(notification_id): def get_notification_by_id(notification_id):
check_suspicious_id(notification_id)
notification = notifications_dao.get_notification_with_personalisation( notification = notifications_dao.get_notification_with_personalisation(
str(authenticated_service.id), notification_id, key_type=None str(authenticated_service.id), notification_id, key_type=None
) )

View File

@@ -27,6 +27,7 @@ from app.organization.organization_schema import (
post_update_invited_org_user_status_schema, post_update_invited_org_user_status_schema,
) )
from app.schema_validation import validate from app.schema_validation import validate
from app.utils import check_suspicious_id
from notifications_utils.url_safe_token import check_token, generate_token from notifications_utils.url_safe_token import check_token, generate_token
organization_invite_blueprint = Blueprint("organization_invite", __name__) organization_invite_blueprint = Blueprint("organization_invite", __name__)
@@ -38,6 +39,7 @@ register_errors(organization_invite_blueprint)
"/organization/<uuid:organization_id>/invite", methods=["POST"] "/organization/<uuid:organization_id>/invite", methods=["POST"]
) )
def invite_user_to_org(organization_id): def invite_user_to_org(organization_id):
check_suspicious_id(organization_id)
data = request.get_json() data = request.get_json()
validate(data, post_create_invited_org_user_status_schema) validate(data, post_create_invited_org_user_status_schema)
@@ -98,6 +100,8 @@ def invite_user_to_org(organization_id):
"/organization/<uuid:organization_id>/invite", methods=["GET"] "/organization/<uuid:organization_id>/invite", methods=["GET"]
) )
def get_invited_org_users_by_organization(organization_id): def get_invited_org_users_by_organization(organization_id):
check_suspicious_id(organization_id)
invited_org_users = get_invited_org_users_for_organization(organization_id) invited_org_users = get_invited_org_users_for_organization(organization_id)
return jsonify(data=[x.serialize() for x in invited_org_users]), 200 return jsonify(data=[x.serialize() for x in invited_org_users]), 200
@@ -106,6 +110,7 @@ def get_invited_org_users_by_organization(organization_id):
"/organization/<uuid:organization_id>/invite/<invited_org_user_id>", methods=["GET"] "/organization/<uuid:organization_id>/invite/<invited_org_user_id>", methods=["GET"]
) )
def get_invited_org_user_by_organization(organization_id, invited_org_user_id): def get_invited_org_user_by_organization(organization_id, invited_org_user_id):
check_suspicious_id(organization_id, invited_org_user_id)
invited_org_user = dao_get_invited_org_user(organization_id, invited_org_user_id) invited_org_user = dao_get_invited_org_user(organization_id, invited_org_user_id)
return jsonify(data=invited_org_user.serialize()), 200 return jsonify(data=invited_org_user.serialize()), 200
@@ -115,6 +120,7 @@ def get_invited_org_user_by_organization(organization_id, invited_org_user_id):
methods=["POST"], methods=["POST"],
) )
def update_org_invite_status(organization_id, invited_org_user_id): def update_org_invite_status(organization_id, invited_org_user_id):
check_suspicious_id(organization_id, invited_org_user_id)
fetched = dao_get_invited_org_user( fetched = dao_get_invited_org_user(
organization_id=organization_id, invited_org_user_id=invited_org_user_id organization_id=organization_id, invited_org_user_id=invited_org_user_id
) )
@@ -129,6 +135,7 @@ def update_org_invite_status(organization_id, invited_org_user_id):
def invited_org_user_url(invited_org_user_id, invite_link_host=None): def invited_org_user_url(invited_org_user_id, invite_link_host=None):
token = generate_token( token = generate_token(
str(invited_org_user_id), str(invited_org_user_id),
current_app.config["SECRET_KEY"], current_app.config["SECRET_KEY"],
@@ -145,6 +152,7 @@ def invited_org_user_url(invited_org_user_id, invite_link_host=None):
"/invite/organization/<uuid:invited_org_user_id>", methods=["GET"] "/invite/organization/<uuid:invited_org_user_id>", methods=["GET"]
) )
def get_invited_org_user(invited_org_user_id): def get_invited_org_user(invited_org_user_id):
check_suspicious_id(invited_org_user_id)
invited_user = get_invited_org_user_by_id(invited_org_user_id) invited_user = get_invited_org_user_by_id(invited_org_user_id)
return jsonify(data=invited_user.serialize()), 200 return jsonify(data=invited_user.serialize()), 200

View File

@@ -36,6 +36,7 @@ from app.organization.organization_schema import (
post_update_organization_schema, post_update_organization_schema,
) )
from app.schema_validation import validate from app.schema_validation import validate
from app.utils import check_suspicious_id
organization_blueprint = Blueprint("organization", __name__) organization_blueprint = Blueprint("organization", __name__)
register_errors(organization_blueprint) register_errors(organization_blueprint)
@@ -64,6 +65,7 @@ def get_organizations():
@organization_blueprint.route("/<uuid:organization_id>", methods=["GET"]) @organization_blueprint.route("/<uuid:organization_id>", methods=["GET"])
def get_organization_by_id(organization_id): def get_organization_by_id(organization_id):
check_suspicious_id(organization_id)
organization = dao_get_organization_by_id(organization_id) organization = dao_get_organization_by_id(organization_id)
return jsonify(organization.serialize()) return jsonify(organization.serialize())
@@ -97,6 +99,7 @@ def create_organization():
@organization_blueprint.route("/<uuid:organization_id>", methods=["POST"]) @organization_blueprint.route("/<uuid:organization_id>", methods=["POST"])
def update_organization(organization_id): def update_organization(organization_id):
check_suspicious_id(organization_id)
data = request.get_json() data = request.get_json()
validate(data, post_update_organization_schema) validate(data, post_update_organization_schema)
@@ -115,6 +118,7 @@ def update_organization(organization_id):
@organization_blueprint.route("/<uuid:organization_id>/service", methods=["POST"]) @organization_blueprint.route("/<uuid:organization_id>/service", methods=["POST"])
def link_service_to_organization(organization_id): def link_service_to_organization(organization_id):
check_suspicious_id(organization_id)
data = request.get_json() data = request.get_json()
validate(data, post_link_service_to_organization_schema) validate(data, post_link_service_to_organization_schema)
service = dao_fetch_service_by_id(data["service_id"]) service = dao_fetch_service_by_id(data["service_id"])
@@ -129,6 +133,7 @@ def link_service_to_organization(organization_id):
@organization_blueprint.route("/<uuid:organization_id>/services", methods=["GET"]) @organization_blueprint.route("/<uuid:organization_id>/services", methods=["GET"])
def get_organization_services(organization_id): def get_organization_services(organization_id):
check_suspicious_id(organization_id)
services = dao_get_organization_services(organization_id) services = dao_get_organization_services(organization_id)
sorted_services = sorted(services, key=lambda s: (-s.active, s.name)) sorted_services = sorted(services, key=lambda s: (-s.active, s.name))
return jsonify([s.serialize_for_org_dashboard() for s in sorted_services]) return jsonify([s.serialize_for_org_dashboard() for s in sorted_services])
@@ -138,6 +143,7 @@ def get_organization_services(organization_id):
"/<uuid:organization_id>/services-with-usage", methods=["GET"] "/<uuid:organization_id>/services-with-usage", methods=["GET"]
) )
def get_organization_services_usage(organization_id): def get_organization_services_usage(organization_id):
check_suspicious_id(organization_id)
try: try:
year = int(request.args.get("year", "none")) year = int(request.args.get("year", "none"))
except ValueError: except ValueError:
@@ -154,6 +160,7 @@ def get_organization_services_usage(organization_id):
"/<uuid:organization_id>/users/<uuid:user_id>", methods=["POST"] "/<uuid:organization_id>/users/<uuid:user_id>", methods=["POST"]
) )
def add_user_to_organization(organization_id, user_id): def add_user_to_organization(organization_id, user_id):
check_suspicious_id(organization_id, user_id)
new_org_user = dao_add_user_to_organization(organization_id, user_id) new_org_user = dao_add_user_to_organization(organization_id, user_id)
return jsonify(data=new_org_user.serialize()) return jsonify(data=new_org_user.serialize())
@@ -162,6 +169,7 @@ def add_user_to_organization(organization_id, user_id):
"/<uuid:organization_id>/users/<uuid:user_id>", methods=["DELETE"] "/<uuid:organization_id>/users/<uuid:user_id>", methods=["DELETE"]
) )
def remove_user_from_organization(organization_id, user_id): def remove_user_from_organization(organization_id, user_id):
check_suspicious_id(organization_id, user_id)
organization = dao_get_organization_by_id(organization_id) organization = dao_get_organization_by_id(organization_id)
user = get_user_by_id(user_id=user_id) user = get_user_by_id(user_id=user_id)
@@ -176,6 +184,7 @@ def remove_user_from_organization(organization_id, user_id):
@organization_blueprint.route("/<uuid:organization_id>/users", methods=["GET"]) @organization_blueprint.route("/<uuid:organization_id>/users", methods=["GET"])
def get_organization_users(organization_id): def get_organization_users(organization_id):
check_suspicious_id(organization_id)
org_users = dao_get_users_for_organization(organization_id) org_users = dao_get_users_for_organization(organization_id)
return jsonify(data=[x.serialize() for x in org_users]) return jsonify(data=[x.serialize() for x in org_users])

View File

@@ -9,6 +9,7 @@ from app.dao.provider_details_dao import (
from app.dao.users_dao import get_user_by_id from app.dao.users_dao import get_user_by_id
from app.errors import InvalidRequest, register_errors from app.errors import InvalidRequest, register_errors
from app.schemas import provider_details_history_schema, provider_details_schema from app.schemas import provider_details_history_schema, provider_details_schema
from app.utils import check_suspicious_id
provider_details = Blueprint("provider_details", __name__) provider_details = Blueprint("provider_details", __name__)
register_errors(provider_details) register_errors(provider_details)
@@ -38,12 +39,14 @@ def get_providers():
@provider_details.route("/<uuid:provider_details_id>", methods=["GET"]) @provider_details.route("/<uuid:provider_details_id>", methods=["GET"])
def get_provider_by_id(provider_details_id): def get_provider_by_id(provider_details_id):
check_suspicious_id(provider_details_id)
data = provider_details_schema.dump(get_provider_details_by_id(provider_details_id)) data = provider_details_schema.dump(get_provider_details_by_id(provider_details_id))
return jsonify(provider_details=data) return jsonify(provider_details=data)
@provider_details.route("/<uuid:provider_details_id>/versions", methods=["GET"]) @provider_details.route("/<uuid:provider_details_id>/versions", methods=["GET"])
def get_provider_versions(provider_details_id): def get_provider_versions(provider_details_id):
check_suspicious_id(provider_details_id)
versions = dao_get_provider_versions(provider_details_id) versions = dao_get_provider_versions(provider_details_id)
data = provider_details_history_schema.dump(versions, many=True) data = provider_details_history_schema.dump(versions, many=True)
return jsonify(data=data) return jsonify(data=data)
@@ -51,6 +54,7 @@ def get_provider_versions(provider_details_id):
@provider_details.route("/<uuid:provider_details_id>", methods=["POST"]) @provider_details.route("/<uuid:provider_details_id>", methods=["POST"])
def update_provider_details(provider_details_id): def update_provider_details(provider_details_id):
check_suspicious_id(provider_details_id)
valid_keys = {"priority", "created_by", "active"} valid_keys = {"priority", "created_by", "active"}
req_json = request.get_json() req_json = request.get_json()

View File

@@ -116,7 +116,7 @@ from app.service.service_senders_schema import (
) )
from app.service.utils import get_guest_list_objects from app.service.utils import get_guest_list_objects
from app.user.users_schema import post_set_permissions_schema from app.user.users_schema import post_set_permissions_schema
from app.utils import get_prev_next_pagination_links, utc_now from app.utils import check_suspicious_id, get_prev_next_pagination_links, utc_now
service_blueprint = Blueprint("service", __name__) service_blueprint = Blueprint("service", __name__)
@@ -202,6 +202,7 @@ def get_live_services_data():
@service_blueprint.route("/<uuid:service_id>", methods=["GET"]) @service_blueprint.route("/<uuid:service_id>", methods=["GET"])
def get_service_by_id(service_id): def get_service_by_id(service_id):
check_suspicious_id(service_id)
if request.args.get("detailed") == "True": if request.args.get("detailed") == "True":
data = get_detailed_service( data = get_detailed_service(
service_id, today_only=request.args.get("today_only") == "True" service_id, today_only=request.args.get("today_only") == "True"
@@ -217,6 +218,7 @@ def get_service_by_id(service_id):
@service_blueprint.route("/<uuid:service_id>/statistics") @service_blueprint.route("/<uuid:service_id>/statistics")
def get_service_notification_statistics(service_id): def get_service_notification_statistics(service_id):
check_suspicious_id(service_id)
return jsonify( return jsonify(
data=get_service_statistics( data=get_service_statistics(
service_id, service_id,
@@ -228,12 +230,14 @@ def get_service_notification_statistics(service_id):
@service_blueprint.route("/<uuid:service_id>/statistics/<string:start>/<int:days>") @service_blueprint.route("/<uuid:service_id>/statistics/<string:start>/<int:days>")
def get_service_notification_statistics_by_day(service_id, start, days): def get_service_notification_statistics_by_day(service_id, start, days):
check_suspicious_id(service_id)
return jsonify( return jsonify(
data=get_service_statistics_for_specific_days(service_id, start, int(days)) data=get_service_statistics_for_specific_days(service_id, start, int(days))
) )
def get_service_statistics_for_specific_days(service_id, start, days=1): def get_service_statistics_for_specific_days(service_id, start, days=1):
check_suspicious_id(service_id)
# Calculate start and end date range # Calculate start and end date range
end_date = datetime.strptime(start, "%Y-%m-%d") end_date = datetime.strptime(start, "%Y-%m-%d")
start_date = end_date - timedelta(days=days - 1) start_date = end_date - timedelta(days=days - 1)
@@ -264,6 +268,7 @@ def get_service_statistics_for_specific_days(service_id, start, days=1):
def get_service_notification_statistics_by_day_by_user( def get_service_notification_statistics_by_day_by_user(
service_id, user_id, start, days service_id, user_id, start, days
): ):
check_suspicious_id(service_id, user_id)
return jsonify( return jsonify(
data=get_service_statistics_for_specific_days_by_user( data=get_service_statistics_for_specific_days_by_user(
service_id, user_id, start, int(days) service_id, user_id, start, int(days)
@@ -323,6 +328,7 @@ def create_service():
@service_blueprint.route("/<uuid:service_id>", methods=["POST"]) @service_blueprint.route("/<uuid:service_id>", methods=["POST"])
def update_service(service_id): def update_service(service_id):
check_suspicious_id(service_id)
req_json = request.get_json() req_json = request.get_json()
fetched_service = dao_fetch_service_by_id(service_id) fetched_service = dao_fetch_service_by_id(service_id)
service_going_live = fetched_service.restricted and not req_json.get( service_going_live = fetched_service.restricted and not req_json.get(
@@ -364,6 +370,8 @@ def update_service(service_id):
@service_blueprint.route("/<uuid:service_id>/api-key", methods=["POST"]) @service_blueprint.route("/<uuid:service_id>/api-key", methods=["POST"])
def create_api_key(service_id=None): def create_api_key(service_id=None):
if service_id:
check_suspicious_id(service_id)
fetched_service = dao_fetch_service_by_id(service_id=service_id) fetched_service = dao_fetch_service_by_id(service_id=service_id)
valid_api_key = api_key_schema.load(request.get_json(), session=db.session) valid_api_key = api_key_schema.load(request.get_json(), session=db.session)
valid_api_key.service = fetched_service valid_api_key.service = fetched_service
@@ -376,6 +384,7 @@ def create_api_key(service_id=None):
"/<uuid:service_id>/api-key/revoke/<uuid:api_key_id>", methods=["POST"] "/<uuid:service_id>/api-key/revoke/<uuid:api_key_id>", methods=["POST"]
) )
def revoke_api_key(service_id, api_key_id): def revoke_api_key(service_id, api_key_id):
check_suspicious_id(service_id, api_key_id)
expire_api_key(service_id=service_id, api_key_id=api_key_id) expire_api_key(service_id=service_id, api_key_id=api_key_id)
return jsonify(), 202 return jsonify(), 202
@@ -383,6 +392,10 @@ def revoke_api_key(service_id, api_key_id):
@service_blueprint.route("/<uuid:service_id>/api-keys", methods=["GET"]) @service_blueprint.route("/<uuid:service_id>/api-keys", methods=["GET"])
@service_blueprint.route("/<uuid:service_id>/api-keys/<uuid:key_id>", methods=["GET"]) @service_blueprint.route("/<uuid:service_id>/api-keys/<uuid:key_id>", methods=["GET"])
def get_api_keys(service_id, key_id=None): def get_api_keys(service_id, key_id=None):
if key_id:
check_suspicious_id(service_id, key_id)
else:
check_suspicious_id(service_id)
dao_fetch_service_by_id(service_id=service_id) dao_fetch_service_by_id(service_id=service_id)
try: try:
@@ -399,12 +412,14 @@ def get_api_keys(service_id, key_id=None):
@service_blueprint.route("/<uuid:service_id>/users", methods=["GET"]) @service_blueprint.route("/<uuid:service_id>/users", methods=["GET"])
def get_users_for_service(service_id): def get_users_for_service(service_id):
check_suspicious_id(service_id)
fetched = dao_fetch_service_by_id(service_id) fetched = dao_fetch_service_by_id(service_id)
return jsonify(data=[x.serialize() for x in fetched.users]) return jsonify(data=[x.serialize() for x in fetched.users])
@service_blueprint.route("/<uuid:service_id>/users/<user_id>", methods=["POST"]) @service_blueprint.route("/<uuid:service_id>/users/<user_id>", methods=["POST"])
def add_user_to_service(service_id, user_id): def add_user_to_service(service_id, user_id):
check_suspicious_id(service_id, user_id)
service = dao_fetch_service_by_id(service_id) service = dao_fetch_service_by_id(service_id)
user = get_user_by_id(user_id=user_id) user = get_user_by_id(user_id=user_id)
if user in service.users: if user in service.users:
@@ -428,6 +443,7 @@ def add_user_to_service(service_id, user_id):
@service_blueprint.route("/<uuid:service_id>/users/<user_id>", methods=["DELETE"]) @service_blueprint.route("/<uuid:service_id>/users/<user_id>", methods=["DELETE"])
def remove_user_from_service(service_id, user_id): def remove_user_from_service(service_id, user_id):
check_suspicious_id(service_id, user_id)
service = dao_fetch_service_by_id(service_id) service = dao_fetch_service_by_id(service_id)
user = get_user_by_id(user_id=user_id) user = get_user_by_id(user_id=user_id)
if user not in service.users: if user not in service.users:
@@ -447,6 +463,7 @@ def remove_user_from_service(service_id, user_id):
# tables. This is so product owner can pass stories as done # tables. This is so product owner can pass stories as done
@service_blueprint.route("/<uuid:service_id>/history", methods=["GET"]) @service_blueprint.route("/<uuid:service_id>/history", methods=["GET"])
def get_service_history(service_id): def get_service_history(service_id):
check_suspicious_id(service_id)
from app.models import ApiKey, Service, TemplateHistory from app.models import ApiKey, Service, TemplateHistory
from app.schemas import ( from app.schemas import (
api_key_history_schema, api_key_history_schema,
@@ -496,6 +513,7 @@ def get_service_history(service_id):
@service_blueprint.route("/<uuid:service_id>/notifications", methods=["GET", "POST"]) @service_blueprint.route("/<uuid:service_id>/notifications", methods=["GET", "POST"])
def get_all_notifications_for_service(service_id): def get_all_notifications_for_service(service_id):
check_suspicious_id(service_id)
current_app.logger.debug("enter get_all_notifications_for_service") current_app.logger.debug("enter get_all_notifications_for_service")
if request.method == "GET": if request.method == "GET":
data = notifications_filter_schema.load(request.args) data = notifications_filter_schema.load(request.args)
@@ -625,6 +643,7 @@ def get_all_notifications_for_service(service_id):
"/<uuid:service_id>/notifications/<uuid:notification_id>", methods=["GET"] "/<uuid:service_id>/notifications/<uuid:notification_id>", methods=["GET"]
) )
def get_notification_for_service(service_id, notification_id): def get_notification_for_service(service_id, notification_id):
check_suspicious_id(service_id, notification_id)
notification = notifications_dao.get_notification_with_personalisation( notification = notifications_dao.get_notification_with_personalisation(
service_id, service_id,
notification_id, notification_id,
@@ -640,6 +659,7 @@ def get_notification_for_service(service_id, notification_id):
@service_blueprint.route("/<uuid:service_id>/notifications/monthly", methods=["GET"]) @service_blueprint.route("/<uuid:service_id>/notifications/monthly", methods=["GET"])
def get_monthly_notification_stats(service_id): def get_monthly_notification_stats(service_id):
check_suspicious_id(service_id)
# check service_id validity # check service_id validity
dao_fetch_service_by_id(service_id) dao_fetch_service_by_id(service_id)
@@ -672,6 +692,7 @@ def get_monthly_notification_stats(service_id):
"/<uuid:service_id>/notifications/<uuid:user_id>/monthly", methods=["GET"] "/<uuid:service_id>/notifications/<uuid:user_id>/monthly", methods=["GET"]
) )
def get_monthly_notification_stats_by_user(service_id, user_id): def get_monthly_notification_stats_by_user(service_id, user_id):
check_suspicious_id(service_id, user_id)
# check service_id validity # check service_id validity
dao_fetch_service_by_id(service_id) dao_fetch_service_by_id(service_id)
# user = get_user_by_id(user_id=user_id) # user = get_user_by_id(user_id=user_id)
@@ -705,6 +726,7 @@ def get_monthly_notification_stats_by_user(service_id, user_id):
"/<uuid:service_id>/notifications/<uuid:user_id>/month", methods=["GET"] "/<uuid:service_id>/notifications/<uuid:user_id>/month", methods=["GET"]
) )
def get_single_month_notification_stats_by_user(service_id, user_id): def get_single_month_notification_stats_by_user(service_id, user_id):
check_suspicious_id(service_id, user_id)
# check service_id validity # check service_id validity
dao_fetch_service_by_id(service_id) dao_fetch_service_by_id(service_id)
@@ -734,6 +756,7 @@ def get_single_month_notification_stats_by_user(service_id, user_id):
@service_blueprint.route("/<uuid:service_id>/notifications/month", methods=["GET"]) @service_blueprint.route("/<uuid:service_id>/notifications/month", methods=["GET"])
def get_single_month_notification_stats_for_service(service_id): def get_single_month_notification_stats_for_service(service_id):
check_suspicious_id(service_id)
# check service_id validity # check service_id validity
dao_fetch_service_by_id(service_id) dao_fetch_service_by_id(service_id)
@@ -757,6 +780,7 @@ def get_single_month_notification_stats_for_service(service_id):
def get_detailed_service(service_id, today_only=False): def get_detailed_service(service_id, today_only=False):
check_suspicious_id(service_id)
service = dao_fetch_service_by_id(service_id) service = dao_fetch_service_by_id(service_id)
service.statistics = get_service_statistics(service_id, today_only) service.statistics = get_service_statistics(service_id, today_only)
@@ -764,6 +788,7 @@ def get_detailed_service(service_id, today_only=False):
def get_service_statistics(service_id, today_only, limit_days=7): def get_service_statistics(service_id, today_only, limit_days=7):
check_suspicious_id(service_id)
# today_only flag is used by the send page to work out if the service will exceed their daily usage by sending a job # today_only flag is used by the send page to work out if the service will exceed their daily usage by sending a job
if today_only: if today_only:
stats = dao_fetch_todays_stats_for_service(service_id) stats = dao_fetch_todays_stats_for_service(service_id)
@@ -810,6 +835,7 @@ def get_detailed_services(
@service_blueprint.route("/<uuid:service_id>/guest-list", methods=["GET"]) @service_blueprint.route("/<uuid:service_id>/guest-list", methods=["GET"])
def get_guest_list(service_id): def get_guest_list(service_id):
check_suspicious_id(service_id)
from app.enums import RecipientType from app.enums import RecipientType
service = dao_fetch_service_by_id(service_id) service = dao_fetch_service_by_id(service_id)
@@ -834,6 +860,7 @@ def get_guest_list(service_id):
@service_blueprint.route("/<uuid:service_id>/guest-list", methods=["PUT"]) @service_blueprint.route("/<uuid:service_id>/guest-list", methods=["PUT"])
def update_guest_list(service_id): def update_guest_list(service_id):
check_suspicious_id(service_id)
# doesn't commit so if there are any errors, we preserve old values in db # doesn't commit so if there are any errors, we preserve old values in db
dao_remove_service_guest_list(service_id) dao_remove_service_guest_list(service_id)
try: try:
@@ -850,6 +877,7 @@ def update_guest_list(service_id):
@service_blueprint.route("/<uuid:service_id>/archive", methods=["POST"]) @service_blueprint.route("/<uuid:service_id>/archive", methods=["POST"])
def archive_service(service_id): def archive_service(service_id):
check_suspicious_id(service_id)
""" """
When a service is archived the service is made inactive, templates are archived and api keys are revoked. When a service is archived the service is made inactive, templates are archived and api keys are revoked.
There is no coming back from this operation. There is no coming back from this operation.
@@ -866,6 +894,7 @@ def archive_service(service_id):
@service_blueprint.route("/<uuid:service_id>/suspend", methods=["POST"]) @service_blueprint.route("/<uuid:service_id>/suspend", methods=["POST"])
def suspend_service(service_id): def suspend_service(service_id):
check_suspicious_id(service_id)
""" """
Suspending a service will mark the service as inactive and revoke API keys. Suspending a service will mark the service as inactive and revoke API keys.
:param service_id: :param service_id:
@@ -881,6 +910,7 @@ def suspend_service(service_id):
@service_blueprint.route("/<uuid:service_id>/resume", methods=["POST"]) @service_blueprint.route("/<uuid:service_id>/resume", methods=["POST"])
def resume_service(service_id): def resume_service(service_id):
check_suspicious_id(service_id)
""" """
Resuming a service that has been suspended will mark the service as active. Resuming a service that has been suspended will mark the service as active.
The service will need to re-create API keys The service will need to re-create API keys
@@ -899,6 +929,7 @@ def resume_service(service_id):
"/<uuid:service_id>/notifications/templates_usage/monthly", methods=["GET"] "/<uuid:service_id>/notifications/templates_usage/monthly", methods=["GET"]
) )
def get_monthly_template_usage(service_id): def get_monthly_template_usage(service_id):
check_suspicious_id(service_id)
try: try:
start_date, end_date = get_calendar_year(int(request.args.get("year", "NaN"))) start_date, end_date = get_calendar_year(int(request.args.get("year", "NaN")))
data = fetch_monthly_template_usage_for_service( data = fetch_monthly_template_usage_for_service(
@@ -924,12 +955,14 @@ def get_monthly_template_usage(service_id):
@service_blueprint.route("/<uuid:service_id>/send-notification", methods=["POST"]) @service_blueprint.route("/<uuid:service_id>/send-notification", methods=["POST"])
def create_one_off_notification(service_id): def create_one_off_notification(service_id):
check_suspicious_id(service_id)
resp = send_one_off_notification(service_id, request.get_json()) resp = send_one_off_notification(service_id, request.get_json())
return jsonify(resp), 201 return jsonify(resp), 201
@service_blueprint.route("/<uuid:service_id>/email-reply-to", methods=["GET"]) @service_blueprint.route("/<uuid:service_id>/email-reply-to", methods=["GET"])
def get_email_reply_to_addresses(service_id): def get_email_reply_to_addresses(service_id):
check_suspicious_id(service_id)
result = dao_get_reply_to_by_service_id(service_id) result = dao_get_reply_to_by_service_id(service_id)
return jsonify([i.serialize() for i in result]), 200 return jsonify([i.serialize() for i in result]), 200
@@ -938,12 +971,14 @@ def get_email_reply_to_addresses(service_id):
"/<uuid:service_id>/email-reply-to/<uuid:reply_to_id>", methods=["GET"] "/<uuid:service_id>/email-reply-to/<uuid:reply_to_id>", methods=["GET"]
) )
def get_email_reply_to_address(service_id, reply_to_id): def get_email_reply_to_address(service_id, reply_to_id):
check_suspicious_id(service_id, reply_to_id)
result = dao_get_reply_to_by_id(service_id=service_id, reply_to_id=reply_to_id) result = dao_get_reply_to_by_id(service_id=service_id, reply_to_id=reply_to_id)
return jsonify(result.serialize()), 200 return jsonify(result.serialize()), 200
@service_blueprint.route("/<uuid:service_id>/email-reply-to/verify", methods=["POST"]) @service_blueprint.route("/<uuid:service_id>/email-reply-to/verify", methods=["POST"])
def verify_reply_to_email_address(service_id): def verify_reply_to_email_address(service_id):
check_suspicious_id(service_id)
email_address = email_data_request_schema.load(request.get_json()) email_address = email_data_request_schema.load(request.get_json())
check_if_reply_to_address_already_in_use(service_id, email_address["email"]) check_if_reply_to_address_already_in_use(service_id, email_address["email"])
@@ -970,6 +1005,7 @@ def verify_reply_to_email_address(service_id):
@service_blueprint.route("/<uuid:service_id>/email-reply-to", methods=["POST"]) @service_blueprint.route("/<uuid:service_id>/email-reply-to", methods=["POST"])
def add_service_reply_to_email_address(service_id): def add_service_reply_to_email_address(service_id):
check_suspicious_id(service_id)
# validate the service exists, throws ResultNotFound exception. # validate the service exists, throws ResultNotFound exception.
dao_fetch_service_by_id(service_id) dao_fetch_service_by_id(service_id)
form = validate(request.get_json(), add_service_email_reply_to_request) form = validate(request.get_json(), add_service_email_reply_to_request)
@@ -986,6 +1022,7 @@ def add_service_reply_to_email_address(service_id):
"/<uuid:service_id>/email-reply-to/<uuid:reply_to_email_id>", methods=["POST"] "/<uuid:service_id>/email-reply-to/<uuid:reply_to_email_id>", methods=["POST"]
) )
def update_service_reply_to_email_address(service_id, reply_to_email_id): def update_service_reply_to_email_address(service_id, reply_to_email_id):
check_suspicious_id(service_id, reply_to_email_id)
# validate the service exists, throws ResultNotFound exception. # validate the service exists, throws ResultNotFound exception.
dao_fetch_service_by_id(service_id) dao_fetch_service_by_id(service_id)
form = validate(request.get_json(), add_service_email_reply_to_request) form = validate(request.get_json(), add_service_email_reply_to_request)
@@ -1003,6 +1040,7 @@ def update_service_reply_to_email_address(service_id, reply_to_email_id):
methods=["POST"], methods=["POST"],
) )
def delete_service_reply_to_email_address(service_id, reply_to_email_id): def delete_service_reply_to_email_address(service_id, reply_to_email_id):
check_suspicious_id(service_id, reply_to_email_id)
archived_reply_to = archive_reply_to_email_address(service_id, reply_to_email_id) archived_reply_to = archive_reply_to_email_address(service_id, reply_to_email_id)
return jsonify(data=archived_reply_to.serialize()), 200 return jsonify(data=archived_reply_to.serialize()), 200
@@ -1010,6 +1048,7 @@ def delete_service_reply_to_email_address(service_id, reply_to_email_id):
@service_blueprint.route("/<uuid:service_id>/sms-sender", methods=["POST"]) @service_blueprint.route("/<uuid:service_id>/sms-sender", methods=["POST"])
def add_service_sms_sender(service_id): def add_service_sms_sender(service_id):
check_suspicious_id(service_id)
dao_fetch_service_by_id(service_id) dao_fetch_service_by_id(service_id)
form = validate(request.get_json(), add_service_sms_sender_request) form = validate(request.get_json(), add_service_sms_sender_request)
inbound_number_id = form.get("inbound_number_id", None) inbound_number_id = form.get("inbound_number_id", None)
@@ -1046,6 +1085,7 @@ def add_service_sms_sender(service_id):
"/<uuid:service_id>/sms-sender/<uuid:sms_sender_id>", methods=["POST"] "/<uuid:service_id>/sms-sender/<uuid:sms_sender_id>", methods=["POST"]
) )
def update_service_sms_sender(service_id, sms_sender_id): def update_service_sms_sender(service_id, sms_sender_id):
check_suspicious_id(service_id, sms_sender_id)
form = validate(request.get_json(), add_service_sms_sender_request) form = validate(request.get_json(), add_service_sms_sender_request)
sms_sender_to_update = dao_get_service_sms_senders_by_id( sms_sender_to_update = dao_get_service_sms_senders_by_id(
@@ -1073,6 +1113,7 @@ def update_service_sms_sender(service_id, sms_sender_id):
"/<uuid:service_id>/sms-sender/<uuid:sms_sender_id>/archive", methods=["POST"] "/<uuid:service_id>/sms-sender/<uuid:sms_sender_id>/archive", methods=["POST"]
) )
def delete_service_sms_sender(service_id, sms_sender_id): def delete_service_sms_sender(service_id, sms_sender_id):
check_suspicious_id(service_id, sms_sender_id)
sms_sender = archive_sms_sender(service_id, sms_sender_id) sms_sender = archive_sms_sender(service_id, sms_sender_id)
return jsonify(data=sms_sender.serialize()), 200 return jsonify(data=sms_sender.serialize()), 200
@@ -1082,6 +1123,8 @@ def delete_service_sms_sender(service_id, sms_sender_id):
"/<uuid:service_id>/sms-sender/<uuid:sms_sender_id>", methods=["GET"] "/<uuid:service_id>/sms-sender/<uuid:sms_sender_id>", methods=["GET"]
) )
def get_service_sms_sender_by_id(service_id, sms_sender_id): def get_service_sms_sender_by_id(service_id, sms_sender_id):
check_suspicious_id(service_id, sms_sender_id)
sms_sender = dao_get_service_sms_senders_by_id( sms_sender = dao_get_service_sms_senders_by_id(
service_id=service_id, service_sms_sender_id=sms_sender_id service_id=service_id, service_sms_sender_id=sms_sender_id
) )
@@ -1090,18 +1133,22 @@ def get_service_sms_sender_by_id(service_id, sms_sender_id):
@service_blueprint.route("/<uuid:service_id>/sms-sender", methods=["GET"]) @service_blueprint.route("/<uuid:service_id>/sms-sender", methods=["GET"])
def get_service_sms_senders_for_service(service_id): def get_service_sms_senders_for_service(service_id):
check_suspicious_id(service_id)
sms_senders = dao_get_sms_senders_by_service_id(service_id=service_id) sms_senders = dao_get_sms_senders_by_service_id(service_id=service_id)
return jsonify([sms_sender.serialize() for sms_sender in sms_senders]), 200 return jsonify([sms_sender.serialize() for sms_sender in sms_senders]), 200
@service_blueprint.route("/<uuid:service_id>/organization", methods=["GET"]) @service_blueprint.route("/<uuid:service_id>/organization", methods=["GET"])
def get_organization_for_service(service_id): def get_organization_for_service(service_id):
check_suspicious_id(service_id)
organization = dao_get_organization_by_service_id(service_id=service_id) organization = dao_get_organization_by_service_id(service_id=service_id)
return jsonify(organization.serialize() if organization else {}), 200 return jsonify(organization.serialize() if organization else {}), 200
@service_blueprint.route("/<uuid:service_id>/data-retention", methods=["GET"]) @service_blueprint.route("/<uuid:service_id>/data-retention", methods=["GET"])
def get_data_retention_for_service(service_id): def get_data_retention_for_service(service_id):
check_suspicious_id(service_id)
data_retention_list = fetch_service_data_retention(service_id) data_retention_list = fetch_service_data_retention(service_id)
return ( return (
jsonify([data_retention.serialize() for data_retention in data_retention_list]), jsonify([data_retention.serialize() for data_retention in data_retention_list]),
@@ -1114,6 +1161,7 @@ def get_data_retention_for_service(service_id):
methods=["GET"], methods=["GET"],
) )
def get_data_retention_for_service_notification_type(service_id, notification_type): def get_data_retention_for_service_notification_type(service_id, notification_type):
check_suspicious_id(service_id)
data_retention = fetch_service_data_retention_by_notification_type( data_retention = fetch_service_data_retention_by_notification_type(
service_id, notification_type service_id, notification_type
) )
@@ -1124,12 +1172,14 @@ def get_data_retention_for_service_notification_type(service_id, notification_ty
"/<uuid:service_id>/data-retention/<uuid:data_retention_id>", methods=["GET"] "/<uuid:service_id>/data-retention/<uuid:data_retention_id>", methods=["GET"]
) )
def get_data_retention_for_service_by_id(service_id, data_retention_id): def get_data_retention_for_service_by_id(service_id, data_retention_id):
check_suspicious_id(service_id, data_retention_id)
data_retention = fetch_service_data_retention_by_id(service_id, data_retention_id) data_retention = fetch_service_data_retention_by_id(service_id, data_retention_id)
return jsonify(data_retention.serialize() if data_retention else {}), 200 return jsonify(data_retention.serialize() if data_retention else {}), 200
@service_blueprint.route("/<uuid:service_id>/data-retention", methods=["POST"]) @service_blueprint.route("/<uuid:service_id>/data-retention", methods=["POST"])
def create_service_data_retention(service_id): def create_service_data_retention(service_id):
check_suspicious_id(service_id)
form = validate(request.get_json(), add_service_data_retention_request) form = validate(request.get_json(), add_service_data_retention_request)
try: try:
new_data_retention = insert_service_data_retention( new_data_retention = insert_service_data_retention(
@@ -1152,6 +1202,7 @@ def create_service_data_retention(service_id):
"/<uuid:service_id>/data-retention/<uuid:data_retention_id>", methods=["POST"] "/<uuid:service_id>/data-retention/<uuid:data_retention_id>", methods=["POST"]
) )
def modify_service_data_retention(service_id, data_retention_id): def modify_service_data_retention(service_id, data_retention_id):
check_suspicious_id(service_id, data_retention_id)
form = validate(request.get_json(), update_service_data_retention_request) form = validate(request.get_json(), update_service_data_retention_request)
update_count = update_service_data_retention( update_count = update_service_data_retention(
@@ -1252,5 +1303,6 @@ def check_if_reply_to_address_already_in_use(service_id, email_address):
@service_blueprint.route("/<uuid:service_id>/notification-count", methods=["GET"]) @service_blueprint.route("/<uuid:service_id>/notification-count", methods=["GET"])
def get_notification_count_for_service_id(service_id): def get_notification_count_for_service_id(service_id):
check_suspicious_id(service_id)
count = dao_get_notification_count_for_service(service_id=service_id) count = dao_get_notification_count_for_service(service_id=service_id)
return jsonify(count=count), 200 return jsonify(count=count), 200

View File

@@ -25,7 +25,7 @@ from app.notifications.process_notifications import (
send_notification_to_queue, send_notification_to_queue,
) )
from app.schemas import InvitedUserSchema from app.schemas import InvitedUserSchema
from app.utils import utc_now from app.utils import check_suspicious_id, utc_now
from notifications_utils.url_safe_token import check_token, generate_token from notifications_utils.url_safe_token import check_token, generate_token
service_invite = Blueprint("service_invite", __name__) service_invite = Blueprint("service_invite", __name__)
@@ -121,6 +121,7 @@ def create_invited_user(service_id):
@service_invite.route("/service/<service_id>/invite/expired", methods=["GET"]) @service_invite.route("/service/<service_id>/invite/expired", methods=["GET"])
def get_expired_invited_users_by_service(service_id): def get_expired_invited_users_by_service(service_id):
check_suspicious_id(service_id)
expired_invited_users = get_expired_invited_users_for_service(service_id) expired_invited_users = get_expired_invited_users_for_service(service_id)
return ( return (
jsonify( jsonify(
@@ -134,6 +135,7 @@ def get_expired_invited_users_by_service(service_id):
@service_invite.route("/service/<service_id>/invite", methods=["GET"]) @service_invite.route("/service/<service_id>/invite", methods=["GET"])
def get_invited_users_by_service(service_id): def get_invited_users_by_service(service_id):
check_suspicious_id(service_id)
invited_users = get_invited_users_for_service(service_id) invited_users = get_invited_users_for_service(service_id)
return ( return (
jsonify( jsonify(
@@ -145,6 +147,7 @@ def get_invited_users_by_service(service_id):
@service_invite.route("/service/<service_id>/invite/<invited_user_id>", methods=["GET"]) @service_invite.route("/service/<service_id>/invite/<invited_user_id>", methods=["GET"])
def get_invited_user_by_service(service_id, invited_user_id): def get_invited_user_by_service(service_id, invited_user_id):
check_suspicious_id(service_id, invited_user_id)
invited_user = get_invited_user_by_service_and_id(service_id, invited_user_id) invited_user = get_invited_user_by_service_and_id(service_id, invited_user_id)
return jsonify(data=InvitedUserSchema(session=db.session).dump(invited_user)), 200 return jsonify(data=InvitedUserSchema(session=db.session).dump(invited_user)), 200
@@ -153,6 +156,7 @@ def get_invited_user_by_service(service_id, invited_user_id):
"/service/<service_id>/invite/<invited_user_id>", methods=["POST"] "/service/<service_id>/invite/<invited_user_id>", methods=["POST"]
) )
def update_invited_user(service_id, invited_user_id): def update_invited_user(service_id, invited_user_id):
check_suspicious_id(service_id, invited_user_id)
fetched = get_invited_user_by_service_and_id( fetched = get_invited_user_by_service_and_id(
service_id=service_id, invited_user_id=invited_user_id service_id=service_id, invited_user_id=invited_user_id
) )
@@ -168,6 +172,7 @@ def update_invited_user(service_id, invited_user_id):
"/service/<service_id>/invite/<invited_user_id>/resend", methods=["POST"] "/service/<service_id>/invite/<invited_user_id>/resend", methods=["POST"]
) )
def resend_service_invite(service_id, invited_user_id): def resend_service_invite(service_id, invited_user_id):
check_suspicious_id(service_id, invited_user_id)
"""Resend an expired invite. """Resend an expired invite.
This resets the invited user's created date and status to make it a new invite, and This resets the invited user's created date and status to make it a new invite, and
@@ -228,6 +233,7 @@ def invited_user_url(invited_user_id, invite_link_host=None):
@service_invite.route("/invite/service/<uuid:invited_user_id>", methods=["GET"]) @service_invite.route("/invite/service/<uuid:invited_user_id>", methods=["GET"])
def get_invited_user(invited_user_id): def get_invited_user(invited_user_id):
check_suspicious_id(invited_user_id)
invited_user = get_invited_user_by_id(invited_user_id) invited_user = get_invited_user_by_id(invited_user_id)
return jsonify(data=InvitedUserSchema(session=db.session).dump(invited_user)), 200 return jsonify(data=InvitedUserSchema(session=db.session).dump(invited_user)), 200

View File

@@ -26,7 +26,7 @@ from app.template.template_schemas import (
post_create_template_schema, post_create_template_schema,
post_update_template_schema, post_update_template_schema,
) )
from app.utils import get_public_notify_type_text from app.utils import check_suspicious_id, get_public_notify_type_text
from notifications_utils import SMS_CHAR_COUNT_LIMIT from notifications_utils import SMS_CHAR_COUNT_LIMIT
from notifications_utils.template import SMSMessageTemplate from notifications_utils.template import SMSMessageTemplate
@@ -61,6 +61,7 @@ def validate_parent_folder(template_json):
@template_blueprint.route("", methods=["POST"]) @template_blueprint.route("", methods=["POST"])
def create_template(service_id): def create_template(service_id):
check_suspicious_id(service_id)
fetched_service = dao_fetch_service_by_id(service_id=service_id) fetched_service = dao_fetch_service_by_id(service_id=service_id)
# permissions needs to be placed here otherwise marshmallow will interfere with versioning # permissions needs to be placed here otherwise marshmallow will interfere with versioning
permissions = [p.permission for p in fetched_service.permissions] permissions = [p.permission for p in fetched_service.permissions]
@@ -96,6 +97,7 @@ def create_template(service_id):
@template_blueprint.route("/<uuid:template_id>", methods=["POST"]) @template_blueprint.route("/<uuid:template_id>", methods=["POST"])
def update_template(service_id, template_id): def update_template(service_id, template_id):
check_suspicious_id(service_id, template_id)
fetched_template = dao_get_template_by_id_and_service_id( fetched_template = dao_get_template_by_id_and_service_id(
template_id=template_id, service_id=service_id template_id=template_id, service_id=service_id
) )
@@ -146,6 +148,7 @@ def update_template(service_id, template_id):
@template_blueprint.route("", methods=["GET"]) @template_blueprint.route("", methods=["GET"])
def get_all_templates_for_service(service_id): def get_all_templates_for_service(service_id):
check_suspicious_id(service_id)
templates = dao_get_all_templates_for_service(service_id=service_id) templates = dao_get_all_templates_for_service(service_id=service_id)
if str(request.args.get("detailed", True)) == "True": if str(request.args.get("detailed", True)) == "True":
data = template_schema.dump(templates, many=True) data = template_schema.dump(templates, many=True)
@@ -156,6 +159,7 @@ def get_all_templates_for_service(service_id):
@template_blueprint.route("/<uuid:template_id>", methods=["GET"]) @template_blueprint.route("/<uuid:template_id>", methods=["GET"])
def get_template_by_id_and_service_id(service_id, template_id): def get_template_by_id_and_service_id(service_id, template_id):
check_suspicious_id(service_id, template_id)
fetched_template = dao_get_template_by_id_and_service_id( fetched_template = dao_get_template_by_id_and_service_id(
template_id=template_id, service_id=service_id template_id=template_id, service_id=service_id
) )
@@ -165,6 +169,7 @@ def get_template_by_id_and_service_id(service_id, template_id):
@template_blueprint.route("/<uuid:template_id>/preview", methods=["GET"]) @template_blueprint.route("/<uuid:template_id>/preview", methods=["GET"])
def preview_template_by_id_and_service_id(service_id, template_id): def preview_template_by_id_and_service_id(service_id, template_id):
check_suspicious_id(service_id, template_id)
fetched_template = dao_get_template_by_id_and_service_id( fetched_template = dao_get_template_by_id_and_service_id(
template_id=template_id, service_id=service_id template_id=template_id, service_id=service_id
) )
@@ -193,6 +198,7 @@ def preview_template_by_id_and_service_id(service_id, template_id):
@template_blueprint.route("/<uuid:template_id>/version/<int:version>") @template_blueprint.route("/<uuid:template_id>/version/<int:version>")
def get_template_version(service_id, template_id, version): def get_template_version(service_id, template_id, version):
check_suspicious_id(service_id, template_id)
data = template_history_schema.dump( data = template_history_schema.dump(
dao_get_template_by_id_and_service_id( dao_get_template_by_id_and_service_id(
template_id=template_id, service_id=service_id, version=version template_id=template_id, service_id=service_id, version=version
@@ -203,6 +209,7 @@ def get_template_version(service_id, template_id, version):
@template_blueprint.route("/<uuid:template_id>/versions") @template_blueprint.route("/<uuid:template_id>/versions")
def get_template_versions(service_id, template_id): def get_template_versions(service_id, template_id):
check_suspicious_id(service_id, template_id)
data = template_history_schema.dump( data = template_history_schema.dump(
dao_get_template_versions(service_id=service_id, template_id=template_id), dao_get_template_versions(service_id=service_id, template_id=template_id),
many=True, many=True,

View File

@@ -20,6 +20,7 @@ from app.template_folder.template_folder_schema import (
post_move_template_folder_schema, post_move_template_folder_schema,
post_update_template_folder_schema, post_update_template_folder_schema,
) )
from app.utils import check_suspicious_id
template_folder_blueprint = Blueprint( template_folder_blueprint = Blueprint(
"template_folder", __name__, url_prefix="/service/<uuid:service_id>/template-folder" "template_folder", __name__, url_prefix="/service/<uuid:service_id>/template-folder"
@@ -37,6 +38,7 @@ def handle_integrity_error(exc):
@template_folder_blueprint.route("", methods=["GET"]) @template_folder_blueprint.route("", methods=["GET"])
def get_template_folders_for_service(service_id): def get_template_folders_for_service(service_id):
check_suspicious_id(service_id)
service = dao_fetch_service_by_id(service_id) service = dao_fetch_service_by_id(service_id)
template_folders = [o.serialize() for o in service.all_template_folders] template_folders = [o.serialize() for o in service.all_template_folders]
@@ -45,6 +47,7 @@ def get_template_folders_for_service(service_id):
@template_folder_blueprint.route("", methods=["POST"]) @template_folder_blueprint.route("", methods=["POST"])
def create_template_folder(service_id): def create_template_folder(service_id):
check_suspicious_id(service_id)
data = request.get_json() data = request.get_json()
validate(data, post_create_template_folder_schema) validate(data, post_create_template_folder_schema)
@@ -72,6 +75,7 @@ def create_template_folder(service_id):
@template_folder_blueprint.route("/<uuid:template_folder_id>", methods=["POST"]) @template_folder_blueprint.route("/<uuid:template_folder_id>", methods=["POST"])
def update_template_folder(service_id, template_folder_id): def update_template_folder(service_id, template_folder_id):
check_suspicious_id(service_id, template_folder_id)
data = request.get_json() data = request.get_json()
validate(data, post_update_template_folder_schema) validate(data, post_update_template_folder_schema)
@@ -93,6 +97,7 @@ def update_template_folder(service_id, template_folder_id):
@template_folder_blueprint.route("/<uuid:template_folder_id>", methods=["DELETE"]) @template_folder_blueprint.route("/<uuid:template_folder_id>", methods=["DELETE"])
def delete_template_folder(service_id, template_folder_id): def delete_template_folder(service_id, template_folder_id):
check_suspicious_id(service_id, template_folder_id)
template_folder = dao_get_template_folder_by_id_and_service_id( template_folder = dao_get_template_folder_by_id_and_service_id(
template_folder_id, service_id template_folder_id, service_id
) )
@@ -112,6 +117,8 @@ def delete_template_folder(service_id, template_folder_id):
) )
@autocommit @autocommit
def move_to_template_folder(service_id, target_template_folder_id=None): def move_to_template_folder(service_id, target_template_folder_id=None):
check_suspicious_id(service_id, target_template_folder_id)
data = request.get_json() data = request.get_json()
validate(data, post_move_template_folder_schema) validate(data, post_move_template_folder_schema)

View File

@@ -6,7 +6,7 @@ from app.dao.fact_notification_status_dao import (
from app.dao.notifications_dao import dao_get_last_date_template_was_used from app.dao.notifications_dao import dao_get_last_date_template_was_used
from app.dao.templates_dao import dao_get_template_by_id_and_service_id from app.dao.templates_dao import dao_get_template_by_id_and_service_id
from app.errors import InvalidRequest, register_errors from app.errors import InvalidRequest, register_errors
from app.utils import DATETIME_FORMAT from app.utils import DATETIME_FORMAT, check_suspicious_id
template_statistics = Blueprint( template_statistics = Blueprint(
"template_statistics", "template_statistics",
@@ -19,6 +19,7 @@ register_errors(template_statistics)
@template_statistics.route("") @template_statistics.route("")
def get_template_statistics_for_service_by_day(service_id): def get_template_statistics_for_service_by_day(service_id):
check_suspicious_id(service_id)
whole_days = request.args.get("whole_days", request.args.get("limit_days", "")) whole_days = request.args.get("whole_days", request.args.get("limit_days", ""))
try: try:
whole_days = int(whole_days) whole_days = int(whole_days)
@@ -56,6 +57,7 @@ def get_template_statistics_for_service_by_day(service_id):
@template_statistics.route("/last-used/<uuid:template_id>") @template_statistics.route("/last-used/<uuid:template_id>")
def get_last_used_datetime_for_template(service_id, template_id): def get_last_used_datetime_for_template(service_id, template_id):
check_suspicious_id(service_id, template_id)
# Check the template and service exist # Check the template and service exist
dao_get_template_by_id_and_service_id(template_id, service_id) dao_get_template_by_id_and_service_id(template_id, service_id)

View File

@@ -4,7 +4,7 @@ from app.dao.fact_notification_status_dao import fetch_notification_statuses_for
from app.dao.jobs_dao import dao_get_notification_outcomes_for_job from app.dao.jobs_dao import dao_get_notification_outcomes_for_job
from app.dao.uploads_dao import dao_get_uploads_by_service_id from app.dao.uploads_dao import dao_get_uploads_by_service_id
from app.errors import register_errors from app.errors import register_errors
from app.utils import midnight_n_days_ago, pagination_links from app.utils import check_suspicious_id, midnight_n_days_ago, pagination_links
upload_blueprint = Blueprint( upload_blueprint = Blueprint(
"upload", __name__, url_prefix="/service/<uuid:service_id>/upload" "upload", __name__, url_prefix="/service/<uuid:service_id>/upload"
@@ -15,6 +15,7 @@ register_errors(upload_blueprint)
@upload_blueprint.route("", methods=["GET"]) @upload_blueprint.route("", methods=["GET"])
def get_uploads_by_service(service_id): def get_uploads_by_service(service_id):
check_suspicious_id(service_id)
return jsonify( return jsonify(
**get_paginated_uploads( **get_paginated_uploads(
service_id, service_id,

View File

@@ -53,7 +53,13 @@ from app.user.users_schema import (
post_verify_code_schema, post_verify_code_schema,
post_verify_webauthn_schema, post_verify_webauthn_schema,
) )
from app.utils import debug_not_production, hilite, url_with_token, utc_now from app.utils import (
check_suspicious_id,
debug_not_production,
hilite,
url_with_token,
utc_now,
)
from notifications_utils.recipients import is_us_phone_number, use_numeric_sender from notifications_utils.recipients import is_us_phone_number, use_numeric_sender
user_blueprint = Blueprint("user", __name__) user_blueprint = Blueprint("user", __name__)
@@ -95,6 +101,7 @@ def create_user():
@user_blueprint.route("/<uuid:user_id>", methods=["POST"]) @user_blueprint.route("/<uuid:user_id>", methods=["POST"])
def update_user_attribute(user_id): def update_user_attribute(user_id):
check_suspicious_id(user_id)
user_to_update = get_user_by_id(user_id=user_id) user_to_update = get_user_by_id(user_id=user_id)
req_json = request.get_json() req_json = request.get_json()
if "updated_by" in req_json: if "updated_by" in req_json:
@@ -159,6 +166,7 @@ def get_sms_reply_to_for_notify_service(recipient, template):
@user_blueprint.route("/<uuid:user_id>/archive", methods=["POST"]) @user_blueprint.route("/<uuid:user_id>/archive", methods=["POST"])
def archive_user(user_id): def archive_user(user_id):
check_suspicious_id(user_id)
user = get_user_by_id(user_id) user = get_user_by_id(user_id)
dao_archive_user(user) dao_archive_user(user)
@@ -167,6 +175,7 @@ def archive_user(user_id):
@user_blueprint.route("/<uuid:user_id>/activate", methods=["POST"]) @user_blueprint.route("/<uuid:user_id>/activate", methods=["POST"])
def activate_user(user_id): def activate_user(user_id):
check_suspicious_id(user_id)
user = get_user_by_id(user_id=user_id) user = get_user_by_id(user_id=user_id)
if user.state == "active": if user.state == "active":
raise InvalidRequest("User already active", status_code=400) raise InvalidRequest("User already active", status_code=400)
@@ -178,6 +187,7 @@ def activate_user(user_id):
@user_blueprint.route("/<uuid:user_id>/deactivate", methods=["POST"]) @user_blueprint.route("/<uuid:user_id>/deactivate", methods=["POST"])
def deactivate_user(user_id): def deactivate_user(user_id):
check_suspicious_id(user_id)
user = get_user_by_id(user_id=user_id) user = get_user_by_id(user_id=user_id)
if user.state == "pending": if user.state == "pending":
raise InvalidRequest("User already inactive", status_code=400) raise InvalidRequest("User already inactive", status_code=400)
@@ -189,6 +199,7 @@ def deactivate_user(user_id):
@user_blueprint.route("/<uuid:user_id>/reset-failed-login-count", methods=["POST"]) @user_blueprint.route("/<uuid:user_id>/reset-failed-login-count", methods=["POST"])
def user_reset_failed_login_count(user_id): def user_reset_failed_login_count(user_id):
check_suspicious_id(user_id)
user_to_update = get_user_by_id(user_id=user_id) user_to_update = get_user_by_id(user_id=user_id)
reset_failed_login_count(user_to_update) reset_failed_login_count(user_to_update)
return jsonify(data=user_to_update.serialize()), 200 return jsonify(data=user_to_update.serialize()), 200
@@ -196,6 +207,7 @@ def user_reset_failed_login_count(user_id):
@user_blueprint.route("/<uuid:user_id>/verify/password", methods=["POST"]) @user_blueprint.route("/<uuid:user_id>/verify/password", methods=["POST"])
def verify_user_password(user_id): def verify_user_password(user_id):
check_suspicious_id(user_id)
user_to_verify = get_user_by_id(user_id=user_id) user_to_verify = get_user_by_id(user_id=user_id)
try: try:
@@ -217,6 +229,7 @@ def verify_user_password(user_id):
@user_blueprint.route("/<uuid:user_id>/verify/code", methods=["POST"]) @user_blueprint.route("/<uuid:user_id>/verify/code", methods=["POST"])
def verify_user_code(user_id): def verify_user_code(user_id):
check_suspicious_id(user_id)
data = request.get_json() data = request.get_json()
validate(data, post_verify_code_schema) validate(data, post_verify_code_schema)
@@ -251,6 +264,7 @@ def verify_user_code(user_id):
@user_blueprint.route("/<uuid:user_id>/complete/webauthn-login", methods=["POST"]) @user_blueprint.route("/<uuid:user_id>/complete/webauthn-login", methods=["POST"])
@user_blueprint.route("/<uuid:user_id>/verify/webauthn-login", methods=["POST"]) @user_blueprint.route("/<uuid:user_id>/verify/webauthn-login", methods=["POST"])
def complete_login_after_webauthn_authentication_attempt(user_id): def complete_login_after_webauthn_authentication_attempt(user_id):
check_suspicious_id(user_id)
""" """
complete login after a webauthn authentication. There's nothing webauthn specific in this code complete login after a webauthn authentication. There's nothing webauthn specific in this code
but the sms/email flows do this as part of `verify_user_code` above and this is the equivalent spot in the but the sms/email flows do this as part of `verify_user_code` above and this is the equivalent spot in the
@@ -283,6 +297,7 @@ def complete_login_after_webauthn_authentication_attempt(user_id):
@user_blueprint.route("/<uuid:user_id>/<code_type>-code", methods=["POST"]) @user_blueprint.route("/<uuid:user_id>/<code_type>-code", methods=["POST"])
def send_user_2fa_code(user_id, code_type): def send_user_2fa_code(user_id, code_type):
check_suspicious_id(user_id)
user_to_send_to = get_user_by_id(user_id=user_id) user_to_send_to = get_user_by_id(user_id=user_id)
if count_user_verify_codes(user_to_send_to) >= current_app.config.get( if count_user_verify_codes(user_to_send_to) >= current_app.config.get(
@@ -386,6 +401,7 @@ def create_2fa_code(
@user_blueprint.route("/<uuid:user_id>/change-email-verification", methods=["POST"]) @user_blueprint.route("/<uuid:user_id>/change-email-verification", methods=["POST"])
def send_user_confirm_new_email(user_id): def send_user_confirm_new_email(user_id):
check_suspicious_id(user_id)
user_to_send_to = get_user_by_id(user_id=user_id) user_to_send_to = get_user_by_id(user_id=user_id)
email = email_data_request_schema.load(request.get_json()) email = email_data_request_schema.load(request.get_json())
@@ -425,6 +441,7 @@ def send_user_confirm_new_email(user_id):
@user_blueprint.route("/<uuid:user_id>/email-verification", methods=["POST"]) @user_blueprint.route("/<uuid:user_id>/email-verification", methods=["POST"])
def send_new_user_email_verification(user_id): def send_new_user_email_verification(user_id):
check_suspicious_id(user_id)
current_app.logger.info("Sending email verification for user {}".format(user_id)) current_app.logger.info("Sending email verification for user {}".format(user_id))
request_json = request.get_json() request_json = request.get_json()
@@ -479,6 +496,7 @@ def send_new_user_email_verification(user_id):
@user_blueprint.route("/<uuid:user_id>/email-already-registered", methods=["POST"]) @user_blueprint.route("/<uuid:user_id>/email-already-registered", methods=["POST"])
def send_already_registered_email(user_id): def send_already_registered_email(user_id):
check_suspicious_id(user_id)
current_app.logger.info("Email already registered for user {}".format(user_id)) current_app.logger.info("Email already registered for user {}".format(user_id))
to = email_data_request_schema.load(request.get_json()) to = email_data_request_schema.load(request.get_json())
@@ -528,6 +546,7 @@ def send_already_registered_email(user_id):
@user_blueprint.route("/<uuid:user_id>", methods=["GET"]) @user_blueprint.route("/<uuid:user_id>", methods=["GET"])
@user_blueprint.route("", methods=["GET"]) @user_blueprint.route("", methods=["GET"])
def get_user(user_id=None): def get_user(user_id=None):
check_suspicious_id(user_id)
users = get_user_by_id(user_id=user_id) users = get_user_by_id(user_id=user_id)
result = ( result = (
[x.serialize() for x in users] if isinstance(users, list) else users.serialize() [x.serialize() for x in users] if isinstance(users, list) else users.serialize()
@@ -539,6 +558,7 @@ def get_user(user_id=None):
"/<uuid:user_id>/service/<uuid:service_id>/permission", methods=["POST"] "/<uuid:user_id>/service/<uuid:service_id>/permission", methods=["POST"]
) )
def set_permissions(user_id, service_id): def set_permissions(user_id, service_id):
check_suspicious_id(user_id, service_id)
# TODO fix security hole, how do we verify that the user # TODO fix security hole, how do we verify that the user
# who is making this request has permission to make the request. # who is making this request has permission to make the request.
service_user = dao_get_service_user(user_id, service_id) service_user = dao_get_service_user(user_id, service_id)
@@ -651,6 +671,7 @@ def report_all_users():
@user_blueprint.route("/<uuid:user_id>/organizations-and-services", methods=["GET"]) @user_blueprint.route("/<uuid:user_id>/organizations-and-services", methods=["GET"])
def get_organizations_and_services_for_user(user_id): def get_organizations_and_services_for_user(user_id):
check_suspicious_id(user_id)
user = get_user_and_accounts(user_id) user = get_user_and_accounts(user_id)
data = get_orgs_and_services(user) data = get_orgs_and_services(user)
return jsonify(data) return jsonify(data)

View File

@@ -1,7 +1,8 @@
import os import os
import re
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from flask import current_app, url_for from flask import abort, current_app, url_for
from sqlalchemy import func from sqlalchemy import func
from notifications_utils.template import HTMLEmailTemplate, SMSMessageTemplate from notifications_utils.template import HTMLEmailTemplate, SMSMessageTemplate
@@ -158,3 +159,52 @@ def emit_job_update_summary(job):
}, },
room=f"job-{job.id}", room=f"job-{job.id}",
) )
def is_suspicious_input(input_str):
if not isinstance(input_str, str):
return False
pattern = re.compile(
r"""
(?i) # case insensite
\b # word boundary
( # start of group for SQL keywords
OR # match SQL keyword OR
|AND
|UNION
|SELECT
|DROP
|INSERT
|UPDATE
|DELETE
|EXEC
|TRUNCATE
|CREATE
|ALTER
|-- # match SQL single-line comment
|/\* # match SQL multi-line comment
|\bpg_sleep\b # Match PostgreSQL 'pg_sleep' function
|\bsleep\b # Match SQL Server 'sleep' function
) # End SQL keywords and function group
| # OR operator to include an alternate pattern
[';]{2,} # Match two or more consecutive single quotes or semi-colons
""",
re.VERBOSE,
)
return bool(re.search(pattern, input_str))
def is_valid_id(id):
if not isinstance(id, str):
return True
return bool(re.match(r"^[a-zA-Z0-9_-]{1,50}$", id))
def check_suspicious_id(*args):
for id in args:
if not is_valid_id(id):
abort(403)
if is_suspicious_input(id):
abort(403)

View File

@@ -9,6 +9,7 @@ from app.dao.webauthn_credential_dao import (
) )
from app.errors import InvalidRequest, register_errors from app.errors import InvalidRequest, register_errors
from app.schema_validation import validate from app.schema_validation import validate
from app.utils import check_suspicious_id
from app.webauthn.webauthn_schema import ( from app.webauthn.webauthn_schema import (
post_create_webauthn_credential_schema, post_create_webauthn_credential_schema,
post_update_webauthn_credential_schema, post_update_webauthn_credential_schema,
@@ -28,6 +29,7 @@ def get_webauthn_credentials(user_id):
@webauthn_blueprint.route("", methods=["POST"]) @webauthn_blueprint.route("", methods=["POST"])
def create_webauthn_credential(user_id): def create_webauthn_credential(user_id):
check_suspicious_id(user_id)
data = request.get_json() data = request.get_json()
validate(data, post_create_webauthn_credential_schema) validate(data, post_create_webauthn_credential_schema)
webauthn_credential = dao_create_webauthn_credential( webauthn_credential = dao_create_webauthn_credential(
@@ -42,6 +44,7 @@ def create_webauthn_credential(user_id):
@webauthn_blueprint.route("/<uuid:webauthn_credential_id>", methods=["POST"]) @webauthn_blueprint.route("/<uuid:webauthn_credential_id>", methods=["POST"])
def update_webauthn_credential(user_id, webauthn_credential_id): def update_webauthn_credential(user_id, webauthn_credential_id):
check_suspicious_id(user_id, webauthn_credential_id)
data = request.get_json() data = request.get_json()
validate(data, post_update_webauthn_credential_schema) validate(data, post_update_webauthn_credential_schema)
@@ -56,6 +59,7 @@ def update_webauthn_credential(user_id, webauthn_credential_id):
@webauthn_blueprint.route("/<uuid:webauthn_credential_id>", methods=["DELETE"]) @webauthn_blueprint.route("/<uuid:webauthn_credential_id>", methods=["DELETE"])
def delete_webauthn_credential(user_id, webauthn_credential_id): def delete_webauthn_credential(user_id, webauthn_credential_id):
check_suspicious_id(user_id, webauthn_credential_id)
webauthn_credential = dao_get_webauthn_credential_by_user_and_id( webauthn_credential = dao_get_webauthn_credential_by_user_and_id(
user_id, webauthn_credential_id user_id, webauthn_credential_id
) )

View File

@@ -5,7 +5,6 @@ from unittest.mock import ANY
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
import pytest import pytest
import werkzeug
from freezegun import freeze_time from freezegun import freeze_time
import app.celery.tasks import app.celery.tasks
@@ -17,7 +16,6 @@ from app.enums import (
NotificationType, NotificationType,
TemplateType, TemplateType,
) )
from app.job.rest import check_suspicious_id, is_suspicious_input, is_valid_id
from app.utils import utc_now from app.utils import utc_now
from tests import create_admin_authorization_header from tests import create_admin_authorization_header
from tests.app.db import ( from tests.app.db import (
@@ -588,31 +586,6 @@ def test_get_all_notifications_for_job_returns_correct_format(
assert resp["notifications"][0]["status"] == sample_notification_with_job.status assert resp["notifications"][0]["status"] == sample_notification_with_job.status
def test_is_valid_id(sample_job):
returnVal = is_valid_id(sample_job.service_id)
assert returnVal is True
returnVal = is_valid_id("abc pgsleep(1)")
assert returnVal is False
def test_check_suspicious_id(sample_job):
# This should be good
check_suspicious_id(sample_job.id, sample_job.service_id)
# This should be bad
with pytest.raises(werkzeug.exceptions.Forbidden):
check_suspicious_id(sample_job.id, "what is this???")
def test_is_suspicious_input(sample_job):
returnVal = is_suspicious_input(sample_job.id)
assert returnVal is False
returnVal = is_suspicious_input("1 OR pg_sleep(1)")
assert returnVal is True
def test_get_notification_count_for_job_id(admin_request, mocker, sample_job): def test_get_notification_count_for_job_id(admin_request, mocker, sample_job):
mock_dao = mocker.patch( mock_dao = mocker.patch(
"app.job.rest.dao_get_notification_count_for_job_id", return_value=3 "app.job.rest.dao_get_notification_count_for_job_id", return_value=3

View File

@@ -1,13 +1,17 @@
from datetime import date, datetime from datetime import date, datetime
import pytest import pytest
import werkzeug
from freezegun import freeze_time from freezegun import freeze_time
from app.enums import ServicePermissionType, TemplateType from app.enums import ServicePermissionType, TemplateType
from app.utils import ( from app.utils import (
check_suspicious_id,
get_midnight_in_utc, get_midnight_in_utc,
get_public_notify_type_text, get_public_notify_type_text,
get_template_instance, get_template_instance,
is_suspicious_input,
is_valid_id,
midnight_n_days_ago, midnight_n_days_ago,
) )
from notifications_utils.template import HTMLEmailTemplate, SMSMessageTemplate from notifications_utils.template import HTMLEmailTemplate, SMSMessageTemplate
@@ -141,3 +145,31 @@ def test_get_template_instance_comprehensive(template_type, values):
assert isinstance(result, SMSMessageTemplate) assert isinstance(result, SMSMessageTemplate)
else: else:
assert isinstance(result, HTMLEmailTemplate) assert isinstance(result, HTMLEmailTemplate)
def test_is_valid_id(sample_job):
returnVal = is_valid_id(sample_job.service_id)
assert returnVal is True
returnVal = is_valid_id("abc pgsleep(1)")
assert returnVal is False
def test_check_suspicious_id(sample_job):
# This should be good
check_suspicious_id(sample_job.id, sample_job.service_id)
# This should be bad
with pytest.raises(werkzeug.exceptions.Forbidden):
check_suspicious_id(sample_job.id, "what is this???")
# This should be good
check_suspicious_id(sample_job.id, None)
def test_is_suspicious_input(sample_job):
returnVal = is_suspicious_input(sample_job.id)
assert returnVal is False
returnVal = is_suspicious_input("1 OR pg_sleep(1)")
assert returnVal is True