Skip to content
9 changes: 6 additions & 3 deletions ldclient/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from ldclient.feature_store import InMemoryFeatureStore
from ldclient.hook import Hook
from ldclient.impl.util import log, validate_application_info
from ldclient.impl.util import log, validate_application_info, validate_sdk_key
from ldclient.interfaces import (
BigSegmentStore,
DataSourceUpdateSink,
Expand Down Expand Up @@ -261,6 +261,9 @@ def __init__(
:param omit_anonymous_contexts: Sets whether anonymous contexts should be omitted from index and identify events.
:param payload_filter_key: The payload filter is used to selectively limited the flags and segments delivered in the data source payload.
"""
if sdk_key and not validate_sdk_key(sdk_key, log):
raise ValueError("SDK key contains invalid characters")
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
Comment thread
jsonbailey marked this conversation as resolved.
Outdated

self.__sdk_key = sdk_key

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

def _validate(self):
if self.offline is False and self.sdk_key is None or self.sdk_key == '':
log.warning("Missing or blank sdk_key.")
if self.offline is False and (self.sdk_key is None or self.sdk_key == ''):
log.warning("Missing or blank SDK key")


__all__ = ['Config', 'BigSegmentsConfig', 'HTTPConfig']
19 changes: 19 additions & 0 deletions ldclient/impl/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,25 @@ def validate_application_value(value: Any, name: str, logger: logging.Logger) ->
return value


def validate_sdk_key(sdk_key: str, logger: logging.Logger) -> bool:
"""
Validate that an SDK key contains only characters that are valid for HTTP headers.
Returns True if valid, False if invalid. Logs a generic error message for invalid keys.
"""
Comment thread
jsonbailey marked this conversation as resolved.
Outdated
if not isinstance(sdk_key, str):
logger.warning("SDK key must be a string")
return False

if sdk_key == '':
return True # Empty keys are handled separately in _validate()

if re.search(r"[^\x21-\x7E]", sdk_key):
Comment thread
jsonbailey marked this conversation as resolved.
Outdated
logger.warning("SDK key contains invalid characters")
return False

return True


def _headers(config):
base_headers = _base_headers(config)
base_headers.update({'Content-Type': "application/json"})
Expand Down
56 changes: 56 additions & 0 deletions ldclient/testing/impl/test_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import logging
from unittest.mock import Mock
from ldclient.impl.util import validate_sdk_key


def test_validate_sdk_key_valid():
"""Test validation of valid SDK keys"""
logger = Mock(spec=logging.Logger)

valid_keys = [
"sdk-12345678-1234-1234-1234-123456789012",
"valid-sdk-key-123",
"VALID_SDK_KEY_456"
]

for key in valid_keys:
assert validate_sdk_key(key, logger) is True
logger.warning.assert_not_called()
logger.reset_mock()


def test_validate_sdk_key_invalid():
"""Test validation of invalid SDK keys"""
logger = Mock(spec=logging.Logger)

invalid_keys = [
"sdk-key-with-\x00-null",
"sdk-key-with-\n-newline",
"sdk-key-with-\t-tab"
]

for key in invalid_keys:
assert validate_sdk_key(key, logger) is False
logger.warning.assert_called_with("SDK key contains invalid characters")
logger.reset_mock()


def test_validate_sdk_key_non_string():
"""Test validation of non-string SDK keys"""
logger = Mock(spec=logging.Logger)

non_string_values = [123, None, object(), [], {}]

for value in non_string_values:
result = validate_sdk_key(value, logger)
assert result is False
logger.warning.assert_called_with("SDK key must be a string")
logger.reset_mock()

Comment thread
cursor[bot] marked this conversation as resolved.

def test_validate_sdk_key_empty():
"""Test validation of empty SDK keys"""
logger = Mock(spec=logging.Logger)

assert validate_sdk_key("", logger) is True
logger.warning.assert_not_called()
33 changes: 33 additions & 0 deletions ldclient/testing/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,39 @@ def test_trims_trailing_slashes_on_uris():
assert config.stream_base_uri == "https://blog.launchdarkly.com"


def test_sdk_key_validation_valid_keys():
"""Test that valid SDK keys are accepted"""
valid_keys = [
"sdk-12345678-1234-1234-1234-123456789012",
"valid-sdk-key-123",
"VALID_SDK_KEY_456"
]

for key in valid_keys:
config = Config(sdk_key=key)
assert config.sdk_key == key


def test_sdk_key_validation_invalid_keys():
"""Test that invalid SDK keys are rejected"""
invalid_keys = [
"sdk-key-with-\x00-null",
"sdk-key-with-\n-newline",
"sdk-key-with-\t-tab",
"sdk-key-with-\x7F-del"
]

for key in invalid_keys:
with pytest.raises(ValueError, match="SDK key contains invalid characters"):
Config(sdk_key=key)


def test_sdk_key_validation_empty_key():
"""Test that empty SDK keys don't trigger format validation"""
config = Config(sdk_key="")
assert config.sdk_key == ""

Comment thread
jsonbailey marked this conversation as resolved.

def application_can_be_set_and_read():
application = {"id": "my-id", "version": "abcdef"}
config = Config(sdk_key="SDK_KEY", application=application)
Expand Down
Loading