Skip to content

Commit 133b3db

Browse files
fix: Add validation format check for SDK key
- Add validate_sdk_key function to prevent logging invalid SDK keys - Validate SDK key format in Config constructor - Add comprehensive tests for SDK key validation - Follow existing validation patterns in codebase Prevents logging of invalid SDK keys by validating that they contain only visible ASCII characters suitable for HTTP headers. Invalid keys trigger a ValueError with a generic message that doesn't expose the actual key value. Co-Authored-By: jbailey@launchdarkly.com <accounts@sidewaysgravity.com>
1 parent eb35610 commit 133b3db

File tree

4 files changed

+109
-3
lines changed

4 files changed

+109
-3
lines changed

ldclient/config.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
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 log, validate_application_info, validate_sdk_key
1313
from ldclient.interfaces import (
1414
BigSegmentStore,
1515
DataSourceUpdateSink,
@@ -261,6 +261,9 @@ def __init__(
261261
:param omit_anonymous_contexts: Sets whether anonymous contexts should be omitted from index and identify events.
262262
:param payload_filter_key: The payload filter is used to selectively limited the flags and segments delivered in the data source payload.
263263
"""
264+
if sdk_key and not validate_sdk_key(sdk_key, log):
265+
raise ValueError("SDK key contains invalid characters")
266+
264267
self.__sdk_key = sdk_key
265268

266269
self.__base_uri = base_uri.rstrip('/')
@@ -542,8 +545,8 @@ def data_source_update_sink(self) -> Optional[DataSourceUpdateSink]:
542545
return self._data_source_update_sink
543546

544547
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.")
548+
if self.offline is False and (self.sdk_key is None or self.sdk_key == ''):
549+
log.warning("Missing or blank SDK key")
547550

548551

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

ldclient/impl/util.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,25 @@ def validate_application_value(value: Any, name: str, logger: logging.Logger) ->
5353
return value
5454

5555

56+
def validate_sdk_key(sdk_key: str, logger: logging.Logger) -> bool:
57+
"""
58+
Validate that an SDK key contains only characters that are valid for HTTP headers.
59+
Returns True if valid, False if invalid. Logs a generic error message for invalid keys.
60+
"""
61+
if not isinstance(sdk_key, str):
62+
logger.warning("SDK key must be a string")
63+
return False
64+
65+
if sdk_key == '':
66+
return True # Empty keys are handled separately in _validate()
67+
68+
if re.search(r"[^\x21-\x7E]", sdk_key):
69+
logger.warning("SDK key contains invalid characters")
70+
return False
71+
72+
return True
73+
74+
5675
def _headers(config):
5776
base_headers = _base_headers(config)
5877
base_headers.update({'Content-Type': "application/json"})

ldclient/testing/impl/test_util.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import logging
2+
from unittest.mock import Mock
3+
from ldclient.impl.util import validate_sdk_key
4+
5+
6+
def test_validate_sdk_key_valid():
7+
"""Test validation of valid SDK keys"""
8+
logger = Mock(spec=logging.Logger)
9+
10+
valid_keys = [
11+
"sdk-12345678-1234-1234-1234-123456789012",
12+
"valid-sdk-key-123",
13+
"VALID_SDK_KEY_456"
14+
]
15+
16+
for key in valid_keys:
17+
assert validate_sdk_key(key, logger) is True
18+
logger.warning.assert_not_called()
19+
logger.reset_mock()
20+
21+
22+
def test_validate_sdk_key_invalid():
23+
"""Test validation of invalid SDK keys"""
24+
logger = Mock(spec=logging.Logger)
25+
26+
invalid_keys = [
27+
"sdk-key-with-\x00-null",
28+
"sdk-key-with-\n-newline",
29+
"sdk-key-with-\t-tab"
30+
]
31+
32+
for key in invalid_keys:
33+
assert validate_sdk_key(key, logger) is False
34+
logger.warning.assert_called_with("SDK key contains invalid characters")
35+
logger.reset_mock()
36+
37+
38+
def test_validate_sdk_key_non_string():
39+
"""Test validation of non-string SDK keys"""
40+
logger = Mock(spec=logging.Logger)
41+
42+
assert validate_sdk_key("123", logger) is True
43+
logger.warning.assert_not_called()
44+
45+
46+
def test_validate_sdk_key_empty():
47+
"""Test validation of empty SDK keys"""
48+
logger = Mock(spec=logging.Logger)
49+
50+
assert validate_sdk_key("", logger) is True
51+
logger.warning.assert_not_called()

ldclient/testing/test_config.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,39 @@ 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+
]
55+
56+
for key in valid_keys:
57+
config = Config(sdk_key=key)
58+
assert config.sdk_key == key
59+
60+
61+
def test_sdk_key_validation_invalid_keys():
62+
"""Test that invalid SDK keys are rejected"""
63+
invalid_keys = [
64+
"sdk-key-with-\x00-null",
65+
"sdk-key-with-\n-newline",
66+
"sdk-key-with-\t-tab",
67+
"sdk-key-with-\x7F-del"
68+
]
69+
70+
for key in invalid_keys:
71+
with pytest.raises(ValueError, match="SDK key contains invalid characters"):
72+
Config(sdk_key=key)
73+
74+
75+
def test_sdk_key_validation_empty_key():
76+
"""Test that empty SDK keys don't trigger format validation"""
77+
config = Config(sdk_key="")
78+
assert config.sdk_key == ""
79+
80+
4881
def application_can_be_set_and_read():
4982
application = {"id": "my-id", "version": "abcdef"}
5083
config = Config(sdk_key="SDK_KEY", application=application)

0 commit comments

Comments
 (0)