Skip to content

Commit bfe4a47

Browse files
fix(rtdb): percent-encode special characters in query values
Query._querystr joined the JSON-encoded query values into the query string as-is, so values containing characters with a special meaning in a URL were not sent as written: equal_to('+15555555') reached the server as " 15555555", equal_to('C#') cut the query at the '#' (dropping orderBy), and '&' or '=' split the value into another parameter. These characters are now percent-encoded.
1 parent de45607 commit bfe4a47

2 files changed

Lines changed: 23 additions & 1 deletion

File tree

‎firebase_admin/db.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import collections
2424
import json
2525
import os
26+
import string
2627
import sys
2728
import threading
2829
from urllib import parse
@@ -71,6 +72,9 @@ def reference(path='/', app=None, url=None):
7172
client = service.get_client(url)
7273
return Reference(client=client, path=path)
7374

75+
_QUERY_VALUE_SAFE_CHARS = ''.join(c for c in string.punctuation if c not in '#%&+=?')
76+
77+
7478
def _parse_path(path):
7579
"""Parses a path string into a set of segments."""
7680
if not isinstance(path, str):
@@ -603,7 +607,10 @@ def equal_to(self, value):
603607
def _querystr(self):
604608
params = []
605609
for key in sorted(self._params):
606-
params.append(f'{key}={self._params[key]}')
610+
# Percent-encode the characters that have a special meaning in a query string
611+
# (e.g. '&', '#' and '+'), so that they are sent as part of the value.
612+
value = parse.quote(str(self._params[key]), safe=_QUERY_VALUE_SAFE_CHARS)
613+
params.append(f'{key}={value}')
607614
return '&'.join(params)
608615

609616
def get(self):

‎tests/test_db.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import os
1919
import sys
2020
import time
21+
from urllib import parse
2122

2223
import pytest
2324

@@ -993,6 +994,20 @@ def test_valid_start_at(self, arg):
993994
query = self.ref.order_by_child('foo').start_at(arg)
994995
assert query._querystr == f'orderBy="foo"&startAt={json.dumps(arg)}'
995996

997+
@pytest.mark.parametrize('value, encoded', [
998+
('C#', '"C%23"'),
999+
('+15555555', '"%2B15555555"'),
1000+
('a&b=c', '"a%26b%3Dc"'),
1001+
('100%', '"100%25"'),
1002+
('what?', '"what%3F"'),
1003+
('a b', '"a%20b"'),
1004+
])
1005+
def test_special_characters_are_encoded(self, value, encoded):
1006+
query = self.ref.order_by_child('foo').equal_to(value)
1007+
assert query._querystr == f'equalTo={encoded}&orderBy="foo"'
1008+
parsed = parse.parse_qs(query._querystr)
1009+
assert json.loads(parsed['equalTo'][0]) == value
1010+
9961011
def test_end_at_none(self):
9971012
query = self.ref.order_by_child('foo')
9981013
with pytest.raises(ValueError):

0 commit comments

Comments
 (0)