Skip to content

Commit b832ea3

Browse files
ejsmithclaude
andcommitted
Restrict environment names to a strict identifier
Names are trimmed, then must match ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$. Anything else is unspecified and never rewritten. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent deba42a commit b832ea3

9 files changed

Lines changed: 90 additions & 47 deletions

File tree

‎.agents/skills/exceptionless-java/SKILL.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ Always recommend importing `com.exceptionless:exceptionless-bom` to align versio
3434
appender to `log4j2.xml`; without `apiKey` it uses the starter's client.
3535
- Deployment environment: `config.environment("production")` is the default, `builder.setEnvironment("staging")`
3636
overrides it per event. Sent as the root `environment` property, separate from `data.@environment`.
37-
Names are trimmed (casing kept); empty, over 64 characters or with control characters are
38-
unspecified. An invalid non-null per-event name (e.g. `" "`) sends the event without an environment
37+
Names are trimmed (casing kept), then must match `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`; anything
38+
else (`Staging West`, `prod/eu`, `produção`, empty, over 64 characters) is unspecified and never rewritten. An invalid non-null per-event name (e.g. `" "`) sends the event without an environment
3939
instead of the default; `setEnvironment(null)` is no override and clears an earlier call, so the default applies. Code beats `EXCEPTIONLESS_ENVIRONMENT`, `exceptionless.environment` and
4040
`exceptionless.properties`; the Spring starter's only active profile is the last fallback.
4141
- `serverUrl(...)` also sets the config and heartbeat URLs; override those after it.

‎README.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,10 +208,11 @@ property or the `EXCEPTIONLESS_ENVIRONMENT` environment variable (or `exceptionl
208208
Spring Boot). A value set in code takes precedence; external configuration applies only when code has
209209
not set one. The Spring Boot starter uses the active profile only when no environment is configured
210210
and exactly one profile is active; a configured empty or invalid value leaves events unspecified
211-
instead. Names are trimmed and limited to 64 characters, preserving the supplied casing. The server
211+
instead. Names are trimmed, then must match `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`: ASCII letters, digits,
212+
`.`, `_` and `-`, starting with a letter or digit, at most 64 characters. Casing is preserved and
213+
names are never rewritten, so `Staging West`, `prod/eu` or `produção` are invalid. The server
212214
filters case-insensitively and normalizes aggregation keys. Missing or invalid names remain
213-
unspecified, and an invalid per-event name (non-null but empty, too long or with control characters)
214-
does not fall back to the configured default; pass `null` to `setEnvironment` to use the default. The
215+
unspecified, and an invalid non-null per-event name does not fall back to the configured default; pass `null` to `setEnvironment` to use the default. The
215216
top-level event `environment` is separate from machine diagnostics in `data.@environment`; stacks and
216217
fixed versions remain shared across environments. Older self hosted servers ignore the property, so
217218
upgrade the server first.

‎exceptionless-core/src/main/java/com/exceptionless/EventBuilder.java‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,11 @@ public ContextData getContextData() {
5656

5757
/**
5858
* Overrides the deployment environment for this event, instead of the configured default. The
59-
* name is trimmed and keeps its casing. A name that is not valid (empty or white space, longer
60-
* than 64 characters or with control characters) is still an override: the event is sent without
61-
* an environment and does not fall back to the default. Null means no override: it clears any
62-
* earlier call on this builder, and the configured default applies.
59+
* name is trimmed and keeps its casing, then must match {@code
60+
* ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$}. A name that is not valid (such as {@code "Staging West"} or
61+
* an empty one) is still an override: the event is sent without an environment and does not fall
62+
* back to the default. Null means no override: it clears any earlier call on this builder, and
63+
* the configured default applies.
6364
*/
6465
public EventBuilder setEnvironment(@Nullable String environment) {
6566
event.setEnvironment(environment);

‎exceptionless-core/src/main/java/com/exceptionless/ExceptionlessConfiguration.java‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -421,9 +421,10 @@ public boolean isEnvironmentConfigured() {
421421
* over {@code exceptionless.properties}, system properties and environment variables, whichever
422422
* is read last.
423423
*
424-
* <p>The name is trimmed and keeps its casing. Empty names, names longer than 64 characters and
425-
* names with control characters leave events without an environment (and still win over external
426-
* configuration). Pass null to forget a value set here.
424+
* <p>The name is trimmed and keeps its casing, then must match {@code
425+
* ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$}. Any other name leaves events without an environment (and
426+
* still wins over external configuration); names are never rewritten. Pass null to forget a value
427+
* set here.
427428
*/
428429
public ExceptionlessConfiguration environment(@Nullable String environment) {
429430
synchronized (environmentLock) {

‎exceptionless-core/src/main/java/com/exceptionless/internal/DeploymentEnvironment.java‎

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
package com.exceptionless.internal;
22

3+
import java.util.regex.Pattern;
34
import org.jspecify.annotations.Nullable;
45

56
/**
6-
* Normalizes deployment environment names the way the server does, so the client never sends a
7-
* value the server would discard.
7+
* Validates deployment environment names the way the server does, so the client never sends a value
8+
* the server would discard.
89
*/
910
public final class DeploymentEnvironment {
10-
/** The longest name the server accepts. */
11-
public static final int MAX_LENGTH = 64;
11+
/** ASCII letters, digits, {@code .}, {@code _} and {@code -}; starts with a letter or digit. */
12+
private static final Pattern VALID_NAME = Pattern.compile("^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$");
1213

1314
private DeploymentEnvironment() {}
1415

1516
/**
16-
* Trims the name and returns it with its casing preserved, or null when the server would treat it
17-
* as unspecified: empty, longer than {@value #MAX_LENGTH} characters or containing a control
18-
* character.
17+
* Trims the name and returns it with its casing preserved, or null when it is not a valid
18+
* deployment environment name: ASCII letters, digits, {@code .}, {@code _} and {@code -},
19+
* starting with a letter or digit, at most 64 characters. Names are never rewritten.
1920
*/
2021
public static @Nullable String normalize(@Nullable String value) {
2122
if (value == null) {
@@ -29,21 +30,13 @@ private DeploymentEnvironment() {}
2930
while (end > start && isTrimmable(value.charAt(end - 1))) {
3031
end--;
3132
}
32-
if (start == end || end - start > MAX_LENGTH) {
33-
return null;
34-
}
35-
for (int i = start; i < end; i++) {
36-
if (Character.isISOControl(value.charAt(i))) {
37-
return null;
38-
}
39-
}
40-
return value.substring(start, end);
33+
String name = value.substring(start, end);
34+
return VALID_NAME.matcher(name).matches() ? name : null;
4135
}
4236

4337
/**
44-
* Whether the server's trim removes the character: white space plus the Unicode space, line and
45-
* paragraph separators. This is wider than {@link String#strip()}, which keeps non-breaking
46-
* spaces.
38+
* Whether the trim removes the character: white space plus the Unicode space, line and paragraph
39+
* separators. This is wider than {@link String#strip()}, which keeps non-breaking spaces.
4740
*/
4841
private static boolean isTrimmable(char c) {
4942
if ((c >= '\t' && c <= '\r') || c == ' ' || c == '\u0085') {

‎exceptionless-core/src/main/java/com/exceptionless/models/Event.java‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ public final class Event implements JsonSerializable {
4141
}
4242

4343
/**
44-
* Sets the deployment environment. The name is trimmed and keeps its casing. Empty names, names
45-
* longer than 64 characters and names with control characters are not valid and clear the
46-
* environment, so the event is sent without one.
44+
* Sets the deployment environment. The name is trimmed and keeps its casing, then must match
45+
* {@code ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$}. Any other name is not valid and clears the
46+
* environment, so the event is sent without one; names are never rewritten.
4747
*/
4848
public void setEnvironment(@Nullable String environment) {
4949
this.environment = DeploymentEnvironment.normalize(environment);

‎exceptionless-core/src/test/java/com/exceptionless/EventEnvironmentTest.java‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,20 @@ void tooLongOverrideSendsTheEventUnspecifiedInsteadOfTheDefault() {
151151
.containsExactlyInAnyOrder(null, "x".repeat(64));
152152
}
153153

154+
@Test
155+
void overrideOutsideTheNamingRuleSendsTheEventUnspecifiedInsteadOfTheDefault() {
156+
config.environment("production");
157+
158+
client.createLog("space").setEnvironment("Staging West").submit();
159+
client.createLog("slash").setEnvironment("prod/eu").submit();
160+
client.createLog("unicode").setEnvironment("produ\u00e7\u00e3o").submit();
161+
client.createLog("ok").setEnvironment("Staging-EU").submit();
162+
163+
assertThat(sent())
164+
.extracting(e -> e.get("environment"))
165+
.containsExactlyInAnyOrder(null, null, null, "Staging-EU");
166+
}
167+
154168
@Test
155169
void invalidDefaultSendsEventsUnspecified() {
156170
config.environment("x".repeat(65));
@@ -252,5 +266,9 @@ void eventSetterNormalizes() {
252266
assertThat(event.getEnvironment()).isEqualTo("Production");
253267
event.setEnvironment("x".repeat(65));
254268
assertThat(event.getEnvironment()).isNull();
269+
event.setEnvironment("Staging-EU");
270+
assertThat(event.getEnvironment()).isEqualTo("Staging-EU");
271+
event.setEnvironment("Staging West");
272+
assertThat(event.getEnvironment()).isNull();
255273
}
256274
}

‎exceptionless-core/src/test/java/com/exceptionless/ExceptionlessConfigurationTest.java‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,16 @@ void environmentIsNormalizedAndKeepsCasing() {
150150
}
151151

152152
@ParameterizedTest
153-
@ValueSource(strings = {"", " ", "prod\nuction"})
153+
@ValueSource(
154+
strings = {
155+
"",
156+
" ",
157+
"prod\nuction",
158+
"Staging West",
159+
"prod/eu",
160+
"-prod",
161+
"produ\u00e7\u00e3o"
162+
})
154163
void invalidEnvironmentIsUnspecifiedButConfigured(String value) {
155164
ExceptionlessConfiguration config = new ExceptionlessConfiguration().environment(value);
156165
assertThat(config.getEnvironment()).isNull();

‎exceptionless-core/src/test/java/com/exceptionless/internal/DeploymentEnvironmentTest.java‎

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,39 +14,59 @@ class DeploymentEnvironmentTest {
1414
delimiter = '|',
1515
value = {
1616
"production|production",
17+
"Staging-EU|Staging-EU",
18+
"qa.2|qa.2",
19+
"dev_local|dev_local",
20+
"a|a",
21+
"0|0",
1722
" Production |Production",
18-
"Prod West|Prod West",
1923
})
20-
void trimsSurroundingWhiteSpaceAndKeepsCasing(String value, String expected) {
24+
void validNamesAreTrimmedAndKeepTheirCasing(String value, String expected) {
2125
assertThat(DeploymentEnvironment.normalize(value)).isEqualTo(expected);
2226
}
2327

2428
@Test
2529
void trimsControlWhiteSpaceAndUnicodeSpaces() {
26-
// The server trims non-breaking and Unicode spaces too, which String.strip() keeps.
2730
assertThat(DeploymentEnvironment.normalize("\tProduction\n")).isEqualTo("Production");
2831
assertThat(DeploymentEnvironment.normalize("\u00a0Production\u00a0")).isEqualTo("Production");
2932
assertThat(DeploymentEnvironment.normalize("\u2003Production\u2028")).isEqualTo("Production");
3033
assertThat(DeploymentEnvironment.normalize("\u0085Production")).isEqualTo("Production");
3134
}
3235

33-
@Test
34-
void keepsInnerCasingAndSpacing() {
35-
assertThat(DeploymentEnvironment.normalize(" Prod West ")).isEqualTo("Prod West");
36-
assertThat(DeploymentEnvironment.normalize(" QA ")).isEqualTo("QA");
37-
}
38-
3936
@ParameterizedTest
4037
@NullSource
41-
@ValueSource(strings = {"", " ", "\t\n", " ", " 
"})
38+
@ValueSource(strings = {"", " ", "\t\n", "\u00a0", " "})
4239
void emptyIsUnspecified(String value) {
4340
assertThat(DeploymentEnvironment.normalize(value)).isNull();
4441
}
4542

43+
@ParameterizedTest
44+
@ValueSource(
45+
strings = {
46+
"Staging West",
47+
" Prod West ",
48+
"prod/eu",
49+
"prod:eu",
50+
"-prod",
51+
".prod",
52+
"_prod",
53+
"produ\u00e7\u00e3o",
54+
"prod\u200b"
55+
})
56+
void namesOutsideTheRuleAreUnspecifiedAndNeverRewritten(String value) {
57+
assertThat(DeploymentEnvironment.normalize(value)).isNull();
58+
}
59+
60+
@Test
61+
void nonAsciiLettersAreUnspecifiedEvenWithinTheLengthLimit() {
62+
assertThat(DeploymentEnvironment.normalize("\u0130".repeat(64))).isNull();
63+
}
64+
4665
@Test
4766
void allowsSixtyFourCharactersAndRejectsSixtyFive() {
48-
assertThat(DeploymentEnvironment.normalize("x".repeat(64))).isEqualTo("x".repeat(64));
49-
assertThat(DeploymentEnvironment.normalize("x".repeat(65))).isNull();
67+
String name = "a" + "b".repeat(63);
68+
assertThat(DeploymentEnvironment.normalize(name)).isEqualTo(name);
69+
assertThat(DeploymentEnvironment.normalize("a".repeat(65))).isNull();
5070
}
5171

5272
@Test

0 commit comments

Comments
 (0)