url_encoding: Use proper parsing for query string appending.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg
2021-10-13 16:45:34 -07:00
committed by Tim Abbott
parent cf7e8e3947
commit f42e191776
8 changed files with 29 additions and 39 deletions

View File

@@ -1,7 +1,6 @@
import urllib
from typing import Any, Dict, List from typing import Any, Dict, List
from urllib.parse import quote, urlsplit
from zerver.lib.pysa import mark_sanitized
from zerver.lib.topic import get_topic_from_message_info from zerver.lib.topic import get_topic_from_message_info
from zerver.models import Realm, Stream, UserProfile from zerver.models import Realm, Stream, UserProfile
@@ -10,7 +9,7 @@ def hash_util_encode(string: str) -> str:
# Do the same encoding operation as hash_util.encodeHashComponent on the # Do the same encoding operation as hash_util.encodeHashComponent on the
# frontend. # frontend.
# `safe` has a default value of "/", but we want those encoded, too. # `safe` has a default value of "/", but we want those encoded, too.
return urllib.parse.quote(string, safe=b"").replace(".", "%2E").replace("%", ".") return quote(string, safe=b"").replace(".", "%2E").replace("%", ".")
def encode_stream(stream_id: int, stream_name: str) -> str: def encode_stream(stream_id: int, stream_name: str) -> str:
@@ -100,14 +99,7 @@ def near_pm_message_url(realm: Realm, message: Dict[str, Any]) -> str:
return full_url return full_url
def add_query_to_redirect_url(original_url: str, query: str) -> str: def append_url_query_string(original_url: str, query: str) -> str:
# Using 'mark_sanitized' because user-controlled data after the '?' is u = urlsplit(original_url)
# not relevant for open redirects query = u.query + ("&" if u.query and query else "") + query
return original_url + "?" + mark_sanitized(query) return u._replace(query=query).geturl()
def add_query_arg_to_redirect_url(original_url: str, query_arg: str) -> str:
assert "?" in original_url
# Using 'mark_sanitized' because user-controlled data after the '?' is
# not relevant for open redirects
return original_url + "&" + mark_sanitized(query_arg)

View File

@@ -63,7 +63,7 @@ from zerver.lib.response import json_success
from zerver.lib.sessions import set_expirable_session_var from zerver.lib.sessions import set_expirable_session_var
from zerver.lib.subdomains import get_subdomain, is_subdomain_root_or_alias from zerver.lib.subdomains import get_subdomain, is_subdomain_root_or_alias
from zerver.lib.types import ViewFuncT from zerver.lib.types import ViewFuncT
from zerver.lib.url_encoding import add_query_to_redirect_url from zerver.lib.url_encoding import append_url_query_string
from zerver.lib.user_agent import parse_user_agent from zerver.lib.user_agent import parse_user_agent
from zerver.lib.users import get_api_key from zerver.lib.users import get_api_key
from zerver.lib.utils import has_api_key_format from zerver.lib.utils import has_api_key_format
@@ -385,9 +385,7 @@ def create_response_for_otp_flow(
} }
# We can't use HttpResponseRedirect, since it only allows HTTP(S) URLs # We can't use HttpResponseRedirect, since it only allows HTTP(S) URLs
response = HttpResponse(status=302) response = HttpResponse(status=302)
response["Location"] = add_query_to_redirect_url( response["Location"] = append_url_query_string("zulip://login", urllib.parse.urlencode(params))
"zulip://login", urllib.parse.urlencode(params)
)
return response return response
@@ -530,7 +528,7 @@ def oauth_redirect_to_root(
params = {**params, **extra_url_params} params = {**params, **extra_url_params}
return redirect(add_query_to_redirect_url(main_site_uri, urllib.parse.urlencode(params))) return redirect(append_url_query_string(main_site_uri, urllib.parse.urlencode(params)))
def handle_desktop_flow(func: ViewFuncT) -> ViewFuncT: def handle_desktop_flow(func: ViewFuncT) -> ViewFuncT:
@@ -554,7 +552,7 @@ def start_remote_user_sso(request: HttpRequest) -> HttpResponse:
to do authentication, so we need this additional endpoint. to do authentication, so we need this additional endpoint.
""" """
query = request.META["QUERY_STRING"] query = request.META["QUERY_STRING"]
return redirect(add_query_to_redirect_url(reverse(remote_user_sso), query)) return redirect(append_url_query_string(reverse(remote_user_sso), query))
@handle_desktop_flow @handle_desktop_flow
@@ -759,7 +757,7 @@ def login_page(
if is_subdomain_root_or_alias(request) and settings.ROOT_DOMAIN_LANDING_PAGE: if is_subdomain_root_or_alias(request) and settings.ROOT_DOMAIN_LANDING_PAGE:
redirect_url = reverse("realm_redirect") redirect_url = reverse("realm_redirect")
if request.GET: if request.GET:
redirect_url = add_query_to_redirect_url(redirect_url, request.GET.urlencode()) redirect_url = append_url_query_string(redirect_url, request.GET.urlencode())
return HttpResponseRedirect(redirect_url) return HttpResponseRedirect(redirect_url)
realm = get_realm_from_request(request) realm = get_realm_from_request(request)
@@ -989,7 +987,7 @@ def logout_then_login(request: HttpRequest, **kwargs: Any) -> HttpResponse:
def password_reset(request: HttpRequest) -> HttpResponse: def password_reset(request: HttpRequest) -> HttpResponse:
if is_subdomain_root_or_alias(request) and settings.ROOT_DOMAIN_LANDING_PAGE: if is_subdomain_root_or_alias(request) and settings.ROOT_DOMAIN_LANDING_PAGE:
redirect_url = add_query_to_redirect_url( redirect_url = append_url_query_string(
reverse("realm_redirect"), urlencode({"next": reverse("password_reset")}) reverse("realm_redirect"), urlencode({"next": reverse("password_reset")})
) )
return HttpResponseRedirect(redirect_url) return HttpResponseRedirect(redirect_url)

View File

@@ -9,7 +9,7 @@ from zerver.lib.exceptions import JsonableError
from zerver.lib.realm_icon import realm_icon_url from zerver.lib.realm_icon import realm_icon_url
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.lib.upload import upload_icon_image from zerver.lib.upload import upload_icon_image
from zerver.lib.url_encoding import add_query_arg_to_redirect_url from zerver.lib.url_encoding import append_url_query_string
from zerver.models import UserProfile from zerver.models import UserProfile
@@ -60,5 +60,5 @@ def get_icon_backend(request: HttpRequest, user_profile: UserProfile) -> HttpRes
# our templates depend on being able to use the ampersand to # our templates depend on being able to use the ampersand to
# add query parameters to our url, get_icon_url does '?version=version_number' # add query parameters to our url, get_icon_url does '?version=version_number'
# hacks to prevent us from having to jump through decode/encode hoops. # hacks to prevent us from having to jump through decode/encode hoops.
url = add_query_arg_to_redirect_url(url, request.META["QUERY_STRING"]) url = append_url_query_string(url, request.META["QUERY_STRING"])
return redirect(url) return redirect(url)

View File

@@ -10,7 +10,7 @@ from zerver.lib.realm_logo import get_realm_logo_url
from zerver.lib.request import REQ, has_request_variables from zerver.lib.request import REQ, has_request_variables
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.lib.upload import upload_logo_image from zerver.lib.upload import upload_logo_image
from zerver.lib.url_encoding import add_query_arg_to_redirect_url from zerver.lib.url_encoding import append_url_query_string
from zerver.lib.validator import check_bool from zerver.lib.validator import check_bool
from zerver.models import UserProfile from zerver.models import UserProfile
@@ -62,5 +62,5 @@ def get_logo_backend(
# our templates depend on being able to use the ampersand to # our templates depend on being able to use the ampersand to
# add query parameters to our url, get_logo_url does '?version=version_number' # add query parameters to our url, get_logo_url does '?version=version_number'
# hacks to prevent us from having to jump through decode/encode hoops. # hacks to prevent us from having to jump through decode/encode hoops.
url = add_query_arg_to_redirect_url(url, request.META["QUERY_STRING"]) url = append_url_query_string(url, request.META["QUERY_STRING"])
return redirect(url) return redirect(url)

View File

@@ -51,7 +51,7 @@ from zerver.lib.request import REQ, has_request_variables
from zerver.lib.send_email import EmailNotDeliveredException, FromAddress, send_email from zerver.lib.send_email import EmailNotDeliveredException, FromAddress, send_email
from zerver.lib.sessions import get_expirable_session_var from zerver.lib.sessions import get_expirable_session_var
from zerver.lib.subdomains import get_subdomain, is_root_domain_available from zerver.lib.subdomains import get_subdomain, is_root_domain_available
from zerver.lib.url_encoding import add_query_to_redirect_url from zerver.lib.url_encoding import append_url_query_string
from zerver.lib.users import get_accounts_for_email from zerver.lib.users import get_accounts_for_email
from zerver.lib.validator import to_converted_or_fallback, to_non_negative_int, to_timezone_or_empty from zerver.lib.validator import to_converted_or_fallback, to_non_negative_int, to_timezone_or_empty
from zerver.lib.zephyr import compute_mit_user_fullname from zerver.lib.zephyr import compute_mit_user_fullname
@@ -405,7 +405,7 @@ def accounts_register(
# is hidden for most users. # is hidden for most users.
view_url = reverse("login") view_url = reverse("login")
query = urlencode({"email": email}) query = urlencode({"email": email})
redirect_url = add_query_to_redirect_url(view_url, query) redirect_url = append_url_query_string(view_url, query)
return HttpResponseRedirect(redirect_url) return HttpResponseRedirect(redirect_url)
elif not realm_creation: elif not realm_creation:
# Since we'll have created a user, we now just log them in. # Since we'll have created a user, we now just log them in.
@@ -574,7 +574,7 @@ def send_confirm_registration_email(
def redirect_to_email_login_url(email: str) -> HttpResponseRedirect: def redirect_to_email_login_url(email: str) -> HttpResponseRedirect:
login_url = reverse("login") login_url = reverse("login")
redirect_url = add_query_to_redirect_url( redirect_url = append_url_query_string(
login_url, urlencode({"email": email, "already_registered": 1}) login_url, urlencode({"email": email, "already_registered": 1})
) )
return HttpResponseRedirect(redirect_url) return HttpResponseRedirect(redirect_url)
@@ -775,7 +775,7 @@ def find_account(
# feature can be used to ascertain which email addresses # feature can be used to ascertain which email addresses
# are associated with Zulip. # are associated with Zulip.
data = urllib.parse.urlencode({"emails": ",".join(emails)}) data = urllib.parse.urlencode({"emails": ",".join(emails)})
return redirect(add_query_to_redirect_url(url, data)) return redirect(append_url_query_string(url, data))
else: else:
form = FindMyTeamForm() form = FindMyTeamForm()
# The below validation is perhaps unnecessary, in that we # The below validation is perhaps unnecessary, in that we

View File

@@ -40,7 +40,7 @@ from zerver.lib.response import json_success
from zerver.lib.streams import access_stream_by_id, access_stream_by_name, subscribed_to_stream from zerver.lib.streams import access_stream_by_id, access_stream_by_name, subscribed_to_stream
from zerver.lib.types import ProfileDataElementValue, Validator from zerver.lib.types import ProfileDataElementValue, Validator
from zerver.lib.upload import upload_avatar_image from zerver.lib.upload import upload_avatar_image
from zerver.lib.url_encoding import add_query_arg_to_redirect_url from zerver.lib.url_encoding import append_url_query_string
from zerver.lib.users import ( from zerver.lib.users import (
access_bot_by_id, access_bot_by_id,
access_user_by_id, access_user_by_id,
@@ -249,7 +249,7 @@ def avatar(
# add query parameters to our url, get_avatar_url does '?x=x' # add query parameters to our url, get_avatar_url does '?x=x'
# hacks to prevent us from having to jump through decode/encode hoops. # hacks to prevent us from having to jump through decode/encode hoops.
assert url is not None assert url is not None
url = add_query_arg_to_redirect_url(url, request.META["QUERY_STRING"]) url = append_url_query_string(url, request.META["QUERY_STRING"])
return redirect(url) return redirect(url)

View File

@@ -29,7 +29,7 @@ from zerver.lib.pysa import mark_sanitized
from zerver.lib.request import REQ, has_request_variables from zerver.lib.request import REQ, has_request_variables
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.lib.subdomains import get_subdomain from zerver.lib.subdomains import get_subdomain
from zerver.lib.url_encoding import add_query_arg_to_redirect_url, add_query_to_redirect_url from zerver.lib.url_encoding import append_url_query_string
from zerver.lib.validator import check_dict, check_string from zerver.lib.validator import check_dict, check_string
from zerver.models import UserProfile, get_realm from zerver.models import UserProfile, get_realm
@@ -191,7 +191,7 @@ def get_bigbluebutton_url(request: HttpRequest, user_profile: UserProfile) -> Ht
+ settings.BIG_BLUE_BUTTON_SECRET + settings.BIG_BLUE_BUTTON_SECRET
).encode() ).encode()
).hexdigest() ).hexdigest()
url = add_query_to_redirect_url( url = append_url_query_string(
"/calls/bigbluebutton/join", "/calls/bigbluebutton/join",
urlencode( urlencode(
{ {
@@ -225,7 +225,7 @@ def join_bigbluebutton(
else: else:
try: try:
response = VideoCallSession().get( response = VideoCallSession().get(
add_query_to_redirect_url( append_url_query_string(
settings.BIG_BLUE_BUTTON_URL + "api/create", settings.BIG_BLUE_BUTTON_URL + "api/create",
urlencode( urlencode(
{ {
@@ -260,7 +260,7 @@ def join_bigbluebutton(
checksum = hashlib.sha1( checksum = hashlib.sha1(
("join" + join_params + settings.BIG_BLUE_BUTTON_SECRET).encode() ("join" + join_params + settings.BIG_BLUE_BUTTON_SECRET).encode()
).hexdigest() ).hexdigest()
redirect_url_base = add_query_to_redirect_url( redirect_url_base = append_url_query_string(
settings.BIG_BLUE_BUTTON_URL + "api/join", join_params settings.BIG_BLUE_BUTTON_URL + "api/join", join_params
) )
return redirect(add_query_arg_to_redirect_url(redirect_url_base, "checksum=" + checksum)) return redirect(append_url_query_string(redirect_url_base, "checksum=" + checksum))

View File

@@ -75,7 +75,7 @@ from zerver.lib.redis_utils import get_dict_from_redis, get_redis_client, put_di
from zerver.lib.request import RequestNotes from zerver.lib.request import RequestNotes
from zerver.lib.subdomains import get_subdomain from zerver.lib.subdomains import get_subdomain
from zerver.lib.types import ProfileDataElementValue from zerver.lib.types import ProfileDataElementValue
from zerver.lib.url_encoding import add_query_to_redirect_url from zerver.lib.url_encoding import append_url_query_string
from zerver.lib.users import check_full_name, validate_user_custom_profile_field from zerver.lib.users import check_full_name, validate_user_custom_profile_field
from zerver.models import ( from zerver.models import (
CustomProfileField, CustomProfileField,
@@ -1429,7 +1429,7 @@ def redirect_deactivated_user_to_login(realm: Realm, email: str) -> HttpResponse
# Specifying the template name makes sure that the user is not redirected to dev_login in case of # Specifying the template name makes sure that the user is not redirected to dev_login in case of
# a deactivated account on a test server. # a deactivated account on a test server.
login_url = reverse("login_page", kwargs={"template_name": "zerver/login.html"}) login_url = reverse("login_page", kwargs={"template_name": "zerver/login.html"})
redirect_url = add_query_to_redirect_url( redirect_url = append_url_query_string(
realm.uri + login_url, urlencode({"is_deactivated": email}) realm.uri + login_url, urlencode({"is_deactivated": email})
) )
return HttpResponseRedirect(redirect_url) return HttpResponseRedirect(redirect_url)