Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit db8facb

Browse files
authoredDec 14, 2023
Issue 48559: WAF encode parameters (#64)
1 parent 2164435 commit db8facb

File tree

7 files changed

+69
-3
lines changed

7 files changed

+69
-3
lines changed
 

‎CHANGE.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
LabKey Python Client API News
33
+++++++++++
44

5+
What's New in the LabKey 3.0.0 package
6+
==============================
7+
8+
*Release date: 12/14/2023*
9+
- Query API - WAF encode "sql" parameter for execute_sql
10+
- WAF encoding of parameters is initially supported with LabKey Server v23.09
11+
- WAF encoding can be opted out of on execute_sql calls by specifying waf_encode_sql=False
12+
513
What's New in the LabKey 2.6.1 package
614
==============================
715

‎labkey/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@
1414
# limitations under the License.
1515
#
1616
__title__ = "labkey"
17-
__version__ = "2.6.1"
17+
__version__ = "3.0.0"
1818
__author__ = "LabKey"
1919
__license__ = "Apache License 2.0"

‎labkey/query.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from typing import List
4545

4646
from .server_context import ServerContext
47+
from .utils import waf_encode
4748

4849
_default_timeout = 60 * 5 # 5 minutes
4950

@@ -231,6 +232,7 @@ def execute_sql(
231232
parameters: dict = None,
232233
required_version: float = None,
233234
timeout: int = _default_timeout,
235+
waf_encode_sql: bool = True
234236
):
235237
"""
236238
Execute sql query against a LabKey server.
@@ -248,11 +250,12 @@ def execute_sql(
248250
:param parameters: parameter values to pass through to a parameterized query
249251
:param required_version: Api version of response
250252
:param timeout: timeout of request in seconds (defaults to 30s)
253+
:param waf_encode_sql: WAF encode sql in request (defaults to True)
251254
:return:
252255
"""
253256
url = server_context.build_url("query", "executeSql.api", container_path=container_path)
254257

255-
payload = {"schemaName": schema_name, "sql": sql}
258+
payload = {"schemaName": schema_name, "sql": waf_encode(sql) if waf_encode_sql else sql}
256259

257260
if container_filter is not None:
258261
payload["containerFilter"] = container_filter
@@ -484,6 +487,7 @@ def execute_sql(
484487
parameters: dict = None,
485488
required_version: float = None,
486489
timeout: int = _default_timeout,
490+
waf_encode_sql: bool = True
487491
):
488492
return execute_sql(
489493
self.server_context,
@@ -498,6 +502,7 @@ def execute_sql(
498502
parameters,
499503
required_version,
500504
timeout,
505+
waf_encode_sql
501506
)
502507

503508
@functools.wraps(insert_rows)

‎labkey/utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import json
1717
from functools import wraps
1818
from datetime import date, datetime
19+
from base64 import b64encode
20+
from urllib import parse
1921

2022

2123
# Issue #14: json.dumps on datetime throws TypeError
@@ -71,3 +73,21 @@ def transform_helper(user_transform_func, file_path_run_properties):
7173
row = [str(el).strip() for el in row]
7274
row = "\t".join(row)
7375
file_out.write(row + "\n")
76+
77+
78+
def btoa(value: str) -> str:
79+
if not value:
80+
return value
81+
binary = value.encode("utf-8")
82+
return b64encode(binary).decode()
83+
84+
85+
def encode_uri_component(value: str) -> str:
86+
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
87+
return parse.quote(value, encoding="utf-8", safe="-_.!~*'()")
88+
89+
90+
def waf_encode(value: str) -> str:
91+
if value:
92+
return "/*{{base64/x-www-form-urlencoded/wafText}}*/" + btoa(encode_uri_component(value))
93+
return value

‎test/integration/test_query.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,14 @@ def test_create_qc_state_definition(qc_states):
101101
assert qc_states["rows"][1]["label"] == "approved"
102102

103103

104+
def test_execute_sql(api: APIWrapper):
105+
resp = api.query.execute_sql("core", "SELECT userId FROM core.users LIMIT 1")
106+
assert resp["schemaName"] == "core"
107+
assert resp["queryName"] == "sql"
108+
assert resp["rowCount"] > 0
109+
assert len(resp["rows"]) > 0
110+
111+
104112
def test_update_qc_state_definition(api: APIWrapper, qc_states, study):
105113
new_description = "for sure that is not right"
106114
edit_rowid = qc_states["rows"][0]["rowid"]

‎test/unit/test_query_api.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
ServerNotFoundError,
3232
RequestAuthorizationError,
3333
)
34+
from labkey.utils import waf_encode
3435

3536
from .utilities import MockLabKey, mock_server_context, success_test, throws_error_test
3637

@@ -297,7 +298,7 @@ def setUp(self):
297298
sql = "select * from " + schema + "." + query
298299
self.expected_kwargs = {
299300
"expected_args": [self.service.get_server_url()],
300-
"data": {"sql": sql, "schemaName": schema},
301+
"data": {"sql": waf_encode(sql), "schemaName": schema},
301302
"headers": None,
302303
"timeout": 300,
303304
}

‎test/unit/test_utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from labkey.utils import btoa, encode_uri_component, waf_encode
2+
3+
4+
def test_btoa():
5+
assert btoa(None) is None
6+
assert btoa("") == ""
7+
assert btoa("DELETE TABLE some.table;") == "REVMRVRFIFRBQkxFIHNvbWUudGFibGU7"
8+
9+
10+
def test_encode_uri_component():
11+
assert(
12+
encode_uri_component("SELECT * FROM x.y WHERE y = 5 & 2 AND y IS NOT NULL;")
13+
== "SELECT%20*%20FROM%20x.y%20WHERE%20y%20%3D%205%20%26%202%20AND%20y%20IS%20NOT%20NULL%3B"
14+
)
15+
assert encode_uri_component("><&/%' \"1äöüÅ") == "%3E%3C%26%2F%25'%20%221%C3%A4%C3%B6%C3%BC%C3%85"
16+
17+
18+
def test_waf_encode():
19+
prefix = "/*{{base64/x-www-form-urlencoded/wafText}}*/"
20+
assert waf_encode(None) is None
21+
assert waf_encode("") == ""
22+
assert waf_encode("hello") == prefix + "aGVsbG8="
23+
assert waf_encode("DELETE TABLE some.table;") == prefix + "REVMRVRFJTIwVEFCTEUlMjBzb21lLnRhYmxlJTNC"
24+
assert waf_encode("><&/%' \"1äöüÅ") == prefix + "JTNFJTNDJTI2JTJGJTI1JyUyMCUyMjElQzMlQTQlQzMlQjYlQzMlQkMlQzMlODU="

0 commit comments

Comments
 (0)
Please sign in to comment.