Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.

Commit 04b9db7

Browse files
authored
improve validation of SDK key so we won't throw an exception that contains the key (#293)
1 parent b66328d commit 04b9db7

File tree

3 files changed

+74
-0
lines changed

3 files changed

+74
-0
lines changed

src/main/java/com/launchdarkly/sdk/server/LDClient.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import static com.launchdarkly.sdk.EvaluationDetail.NO_VARIATION;
4242
import static com.launchdarkly.sdk.server.DataModel.FEATURES;
4343
import static com.launchdarkly.sdk.server.DataModel.SEGMENTS;
44+
import static com.launchdarkly.sdk.server.Util.isAsciiHeaderValue;
4445

4546
/**
4647
* A client for the LaunchDarkly API. Client instances are thread-safe. Applications should instantiate
@@ -82,8 +83,15 @@ public final class LDClient implements LDClientInterface {
8283
* values; it will still continue trying to connect in the background. You can detect whether
8384
* initialization has succeeded by calling {@link #isInitialized()}. If you prefer to customize
8485
* this behavior, use {@link LDClient#LDClient(String, LDConfig)} instead.
86+
* <p>
87+
* For rules regarding the throwing of unchecked exceptions for error conditions, see
88+
* {@link LDClient#LDClient(String, LDConfig)}.
8589
*
8690
* @param sdkKey the SDK key for your LaunchDarkly environment
91+
* @throws IllegalArgumentException if a parameter contained a grossly malformed value;
92+
* for security reasons, in case of an illegal SDK key, the exception message does
93+
* not include the key
94+
* @throws NullPointerException if a non-nullable parameter was null
8795
* @see LDClient#LDClient(String, LDConfig)
8896
*/
8997
public LDClient(String sdkKey) {
@@ -136,14 +144,32 @@ private static final DataModel.Segment getSegment(DataStore store, String key) {
136144
* // do whatever is appropriate if initialization has timed out
137145
* }
138146
* </code></pre>
147+
* <p>
148+
* This constructor can throw unchecked exceptions if it is immediately apparent that
149+
* the SDK cannot work with these parameters. For instance, if the SDK key contains a
150+
* non-printable character that cannot be used in an HTTP header, it will throw an
151+
* {@link IllegalArgumentException} since the SDK key is normally sent to LaunchDarkly
152+
* in an HTTP header and no such value could possibly be valid. Similarly, a null
153+
* value for a non-nullable parameter may throw a {@link NullPointerException}. The
154+
* constructor will not throw an exception for any error condition that could only be
155+
* detected after making a request to LaunchDarkly (such as an SDK key that is simply
156+
* wrong despite being valid ASCII, so it is invalid but not illegal); those are logged
157+
* and treated as an unsuccessful initialization, as described above.
139158
*
140159
* @param sdkKey the SDK key for your LaunchDarkly environment
141160
* @param config a client configuration object
161+
* @throws IllegalArgumentException if a parameter contained a grossly malformed value;
162+
* for security reasons, in case of an illegal SDK key, the exception message does
163+
* not include the key
164+
* @throws NullPointerException if a non-nullable parameter was null
142165
* @see LDClient#LDClient(String, LDConfig)
143166
*/
144167
public LDClient(String sdkKey, LDConfig config) {
145168
checkNotNull(config, "config must not be null");
146169
this.sdkKey = checkNotNull(sdkKey, "sdkKey must not be null");
170+
if (!isAsciiHeaderValue(sdkKey) ) {
171+
throw new IllegalArgumentException("SDK key contained an invalid character");
172+
}
147173
this.offline = config.offline;
148174

149175
this.sharedExecutor = createSharedExecutor(config);

src/main/java/com/launchdarkly/sdk/server/Util.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,25 @@ static Headers.Builder getHeadersBuilderFor(HttpConfiguration config) {
3737
return builder;
3838
}
3939

40+
// This is specifically testing whether the string would be considered a valid HTTP header value
41+
// *by the OkHttp client*. The actual HTTP spec does not prohibit characters >= 127; OkHttp's
42+
// check is overly strict, as was pointed out in https://github.com/square/okhttp/issues/2016.
43+
// But all OkHttp 3.x and 4.x versions so far have continued to enforce that check. Control
44+
// characters other than a tab are always illegal.
45+
//
46+
// The value we're mainly concerned with is the SDK key (Authorization header). If an SDK key
47+
// accidentally has (for instance) a newline added to it, we don't want to end up having OkHttp
48+
// throw an exception mentioning the value, which might get logged (https://github.com/square/okhttp/issues/6738).
49+
static boolean isAsciiHeaderValue(String value) {
50+
for (int i = 0; i < value.length(); i++) {
51+
char ch = value.charAt(i);
52+
if ((ch < 0x20 || ch > 0x7e) && ch != '\t') {
53+
return false;
54+
}
55+
}
56+
return true;
57+
}
58+
4059
static void configureHttpClientBuilder(HttpConfiguration config, OkHttpClient.Builder builder) {
4160
builder.connectionPool(new ConnectionPool(5, 5, TimeUnit.SECONDS))
4261
.connectTimeout(config.getConnectTimeout())

src/test/java/com/launchdarkly/sdk/server/LDClientTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
import static org.easymock.EasyMock.capture;
3838
import static org.easymock.EasyMock.expect;
3939
import static org.easymock.EasyMock.isA;
40+
import static org.hamcrest.MatcherAssert.assertThat;
41+
import static org.hamcrest.Matchers.containsString;
42+
import static org.hamcrest.Matchers.not;
4043
import static org.junit.Assert.assertEquals;
4144
import static org.junit.Assert.assertFalse;
4245
import static org.junit.Assert.assertNotNull;
@@ -83,6 +86,32 @@ public void constructorWithConfigThrowsExceptionForNullSdkKey() throws Exception
8386
}
8487
}
8588

89+
@Test
90+
public void constructorThrowsExceptionForSdkKeyWithControlCharacter() throws Exception {
91+
try (LDClient client = new LDClient(SDK_KEY + "\n")) {
92+
fail("expected exception");
93+
} catch (IllegalArgumentException e) {
94+
assertThat(e.getMessage(), not(containsString(SDK_KEY)));
95+
}
96+
}
97+
98+
@Test
99+
public void constructorWithConfigThrowsExceptionForSdkKeyWithControlCharacter() throws Exception {
100+
try (LDClient client = new LDClient(SDK_KEY + "\n", LDConfig.DEFAULT)) {
101+
fail("expected exception");
102+
} catch (IllegalArgumentException e) {
103+
assertThat(e.getMessage(), not(containsString(SDK_KEY)));
104+
}
105+
}
106+
107+
@Test
108+
public void constructorAllowsSdkKeyToBeEmpty() throws Exception {
109+
// It may seem counter-intuitive to allow this, but if someone is using the SDK in offline
110+
// mode, or with a file data source or a test fixture, they may reasonably assume that it's
111+
// OK to pass an empty string since the key won't actually be used.
112+
try (LDClient client = new LDClient(SDK_KEY + "")) {}
113+
}
114+
86115
@Test
87116
public void constructorThrowsExceptionForNullConfig() throws Exception {
88117
try (LDClient client = new LDClient(SDK_KEY, null)) {

0 commit comments

Comments
 (0)