Skip to content

Commit 4c8d4ae

Browse files
authored
fix: Add validation format check for SDK key (#351)
2 parents 9c6289a + 6ec6770 commit 4c8d4ae

File tree

4 files changed

+169
-7
lines changed

4 files changed

+169
-7
lines changed

ldclient/config.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99

1010
from ldclient.feature_store import InMemoryFeatureStore
1111
from ldclient.hook import Hook
12-
from ldclient.impl.util import log, validate_application_info
12+
from ldclient.impl.util import (
13+
log,
14+
validate_application_info,
15+
validate_sdk_key_format
16+
)
1317
from ldclient.interfaces import (
1418
BigSegmentStore,
1519
DataSourceUpdateSink,
@@ -261,7 +265,7 @@ def __init__(
261265
:param omit_anonymous_contexts: Sets whether anonymous contexts should be omitted from index and identify events.
262266
:param payload_filter_key: The payload filter is used to selectively limited the flags and segments delivered in the data source payload.
263267
"""
264-
self.__sdk_key = sdk_key
268+
self.__sdk_key = validate_sdk_key_format(sdk_key, log)
265269

266270
self.__base_uri = base_uri.rstrip('/')
267271
self.__events_uri = events_uri.rstrip('/')
@@ -302,6 +306,7 @@ def __init__(
302306

303307
def copy_with_new_sdk_key(self, new_sdk_key: str) -> 'Config':
304308
"""Returns a new ``Config`` instance that is the same as this one, except for having a different SDK key.
309+
The key will not be updated if the provided key contains invalid characters.
305310
306311
:param new_sdk_key: the new SDK key
307312
"""
@@ -542,8 +547,8 @@ def data_source_update_sink(self) -> Optional[DataSourceUpdateSink]:
542547
return self._data_source_update_sink
543548

544549
def _validate(self):
545-
if self.offline is False and self.sdk_key is None or self.sdk_key == '':
546-
log.warning("Missing or blank sdk_key.")
550+
if self.offline is False and self.sdk_key == '':
551+
log.warning("Missing or blank SDK key")
547552

548553

549554
__all__ = ['Config', 'BigSegmentsConfig', 'HTTPConfig']

ldclient/impl/util.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,13 @@ def timedelta_millis(delta: timedelta) -> float:
2727

2828
__BASE_TYPES__ = (str, float, int, bool)
2929

30+
# Maximum length for SDK keys
31+
_MAX_SDK_KEY_LENGTH = 8192
3032

31-
_retryable_statuses = [400, 408, 429]
33+
_RETRYABLE_STATUSES = [400, 408, 429]
34+
35+
# Compiled regex pattern for valid characters in application values and SDK keys
36+
_VALID_CHARACTERS_REGEX = re.compile(r"[^a-zA-Z0-9._-]")
3237

3338

3439
def validate_application_info(application: dict, logger: logging.Logger) -> dict:
@@ -46,13 +51,35 @@ def validate_application_value(value: Any, name: str, logger: logging.Logger) ->
4651
logger.warning('Value of application[%s] was longer than 64 characters and was discarded' % name)
4752
return ""
4853

49-
if re.search(r"[^a-zA-Z0-9._-]", value):
54+
if _VALID_CHARACTERS_REGEX.search(value):
5055
logger.warning('Value of application[%s] contained invalid characters and was discarded' % name)
5156
return ""
5257

5358
return value
5459

5560

61+
def validate_sdk_key_format(sdk_key: str, logger: logging.Logger) -> str:
62+
"""
63+
Validates that an SDK key does not contain invalid characters and is not too long for our systems.
64+
65+
:param sdk_key: the SDK key to validate
66+
:param logger: the logger to use for logging warnings
67+
:return: the validated SDK key, or empty string if the SDK key is invalid
68+
"""
69+
if sdk_key is None or sdk_key == '':
70+
return ""
71+
72+
if not isinstance(sdk_key, str):
73+
return ""
74+
if len(sdk_key) > _MAX_SDK_KEY_LENGTH:
75+
logger.warning('SDK key was longer than %d characters and was discarded' % _MAX_SDK_KEY_LENGTH)
76+
return ""
77+
if _VALID_CHARACTERS_REGEX.search(sdk_key):
78+
logger.warning('SDK key contained invalid characters and was discarded')
79+
return ""
80+
return sdk_key
81+
82+
5683
def _headers(config):
5784
base_headers = _base_headers(config)
5885
base_headers.update({'Content-Type': "application/json"})
@@ -106,7 +133,7 @@ def throw_if_unsuccessful_response(resp):
106133

107134
def is_http_error_recoverable(status):
108135
if status >= 400 and status < 500:
109-
return status in _retryable_statuses # all other 4xx besides these are unrecoverable
136+
return status in _RETRYABLE_STATUSES # all other 4xx besides these are unrecoverable
110137
return True # all other errors are recoverable
111138

112139

ldclient/testing/impl/test_util.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import logging
2+
3+
from ldclient.impl.util import validate_sdk_key_format
4+
5+
6+
def test_validate_sdk_key_format_valid():
7+
"""Test validation of valid SDK keys"""
8+
logger = logging.getLogger('test')
9+
valid_keys = [
10+
"sdk-12345678-1234-1234-1234-123456789012",
11+
"valid-sdk-key-123",
12+
"VALID_SDK_KEY_456",
13+
"test.key_with.dots",
14+
"test-key-with-hyphens"
15+
]
16+
17+
for key in valid_keys:
18+
result = validate_sdk_key_format(key, logger)
19+
assert result == key # Should return the same key if valid
20+
21+
22+
def test_validate_sdk_key_format_invalid():
23+
"""Test validation of invalid SDK keys"""
24+
logger = logging.getLogger('test')
25+
invalid_keys = [
26+
"sdk-key-with-\x00-null",
27+
"sdk-key-with-\n-newline",
28+
"sdk-key-with-\t-tab",
29+
"sdk key with spaces",
30+
"sdk@key#with$special%chars",
31+
"sdk/key\\with/slashes"
32+
]
33+
34+
for key in invalid_keys:
35+
result = validate_sdk_key_format(key, logger)
36+
assert result == '' # Should return empty string for invalid keys
37+
38+
39+
def test_validate_sdk_key_format_non_string():
40+
"""Test validation of non-string SDK keys"""
41+
logger = logging.getLogger('test')
42+
non_string_values = [123, object(), [], {}]
43+
44+
for value in non_string_values:
45+
result = validate_sdk_key_format(value, logger)
46+
assert result == '' # Should return empty string for non-string values
47+
48+
49+
def test_validate_sdk_key_format_empty_and_none():
50+
"""Test validation of empty and None SDK keys"""
51+
logger = logging.getLogger('test')
52+
assert validate_sdk_key_format("", logger) == '' # Empty string should return empty string
53+
assert validate_sdk_key_format(None, logger) == '' # None should return empty string
54+
55+
56+
def test_validate_sdk_key_format_max_length():
57+
"""Test validation of SDK key maximum length"""
58+
logger = logging.getLogger('test')
59+
valid_key = "a" * 8192
60+
result = validate_sdk_key_format(valid_key, logger)
61+
assert result == valid_key # Should return the same key if valid
62+
63+
invalid_key = "a" * 8193
64+
result = validate_sdk_key_format(invalid_key, logger)
65+
assert result == '' # Should return empty string for keys that are too long

ldclient/testing/test_config.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,71 @@ def test_trims_trailing_slashes_on_uris():
4545
assert config.stream_base_uri == "https://blog.launchdarkly.com"
4646

4747

48+
def test_sdk_key_validation_valid_keys():
49+
"""Test that valid SDK keys are accepted"""
50+
valid_keys = [
51+
"sdk-12345678-1234-1234-1234-123456789012",
52+
"valid-sdk-key-123",
53+
"VALID_SDK_KEY_456",
54+
"test.key_with.dots",
55+
"test-key-with-hyphens"
56+
]
57+
58+
for key in valid_keys:
59+
config = Config(sdk_key=key)
60+
assert config.sdk_key == key
61+
62+
63+
def test_sdk_key_validation_invalid_keys():
64+
"""Test that invalid SDK keys are not set"""
65+
invalid_keys = [
66+
"sdk-key-with-\x00-null",
67+
"sdk-key-with-\n-newline",
68+
"sdk-key-with-\t-tab",
69+
"sdk key with spaces",
70+
"sdk@key#with$special%chars",
71+
"sdk/key\\with/slashes"
72+
]
73+
74+
for key in invalid_keys:
75+
config = Config(sdk_key=key)
76+
assert config.sdk_key == ''
77+
78+
79+
def test_sdk_key_validation_empty_key():
80+
"""Test that empty SDK keys are accepted"""
81+
config = Config(sdk_key="")
82+
assert config.sdk_key == ""
83+
84+
85+
def test_sdk_key_validation_none_key():
86+
"""Test that None SDK keys are accepted"""
87+
config = Config(sdk_key=None)
88+
assert config.sdk_key == ''
89+
90+
91+
def test_sdk_key_validation_max_length():
92+
"""Test SDK key maximum length validation"""
93+
valid_key = "a" * 8192
94+
config = Config(sdk_key=valid_key)
95+
assert config.sdk_key == valid_key
96+
97+
invalid_key = "a" * 8193
98+
config = Config(sdk_key=invalid_key)
99+
assert config.sdk_key == ''
100+
101+
102+
def test_copy_with_new_sdk_key_validation():
103+
"""Test that copy_with_new_sdk_key validates the new key"""
104+
original_config = Config(sdk_key="valid-key")
105+
106+
new_config = original_config.copy_with_new_sdk_key("another-valid-key")
107+
assert new_config.sdk_key == "another-valid-key"
108+
109+
invalid_config = original_config.copy_with_new_sdk_key("invalid key with spaces")
110+
assert invalid_config.sdk_key == ''
111+
112+
48113
def application_can_be_set_and_read():
49114
application = {"id": "my-id", "version": "abcdef"}
50115
config = Config(sdk_key="SDK_KEY", application=application)

0 commit comments

Comments
 (0)