Skip to content

Commit 73eb8f2

Browse files
Reject non-ASCII whitespace in idn-email and strengthen email format tests (#1268)
* Reject non-ASCII whitespace in idn-email and strengthen email format tests Follow-up to #1267 applying the review suggestions: - Extract the non-ASCII-whitespace check to Strings.containsNonAsciiWhitespace and reuse it from both EmailFormat and IdnEmailFormat. - idn-email shares EmailFormat's delegation and had the same bug (a leading U+00A0 was accepted); it now rejects non-ASCII whitespace too. Adds a license header to IdnEmailFormat (it had none) and a new IdnEmailFormatTest that also verifies a non-ASCII letter is still accepted. - Strengthen EmailFormatTest: assert a quoted local part with an ASCII space ("joe bloggs"@example.com) stays valid, and generalize the negative case over U+00A0, U+2003 and U+3000. The change stays deliberately narrow (whitespace only); non-ASCII letters are still accepted in idn-email. Zero-width format characters (e.g. U+200B, U+FEFF) are out of scope and unchanged. * Generalize idn-email whitespace test over U+2003 and U+3000 --------- Co-authored-by: el-psy-kongroo-d <307969302+el-psy-kongroo-d@users.noreply.github.com>
1 parent d97a368 commit 73eb8f2

5 files changed

Lines changed: 160 additions & 18 deletions

File tree

src/main/java/com/networknt/schema/format/EmailFormat.java

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.networknt.org.apache.commons.validator.routines.EmailValidator;
2020
import com.networknt.schema.ExecutionContext;
21+
import com.networknt.schema.utils.Strings;
2122

2223
/**
2324
* Format for email.
@@ -35,26 +36,11 @@ public EmailFormat(EmailValidator emailValidator) {
3536

3637
@Override
3738
public boolean matches(ExecutionContext executionContext, String value) {
38-
if (containsNonAsciiWhitespace(value)) {
39+
if (Strings.containsNonAsciiWhitespace(value)) {
3940
return false;
4041
}
4142
return this.emailValidator.isValid(value);
4243
}
43-
44-
/**
45-
* Whether {@code value} contains a non-ASCII whitespace character such as
46-
* {@code U+00A0} NON-BREAKING SPACE. The underlying validator only excludes
47-
* ASCII whitespace, so such characters would otherwise be accepted in an
48-
* email address. Note that {@link Character#isWhitespace(char)} deliberately
49-
* excludes {@code U+00A0}, hence the additional
50-
* {@link Character#isSpaceChar(char)} check.
51-
*/
52-
private static boolean containsNonAsciiWhitespace(String value) {
53-
if (value == null) {
54-
return false;
55-
}
56-
return value.codePoints().anyMatch(ch -> ch > 0x7F && (Character.isWhitespace(ch) || Character.isSpaceChar(ch)));
57-
}
5844

5945
@Override
6046
public String getName() {

src/main/java/com/networknt/schema/format/IdnEmailFormat.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,24 @@
1+
/*
2+
* Copyright (c) 2016 Network New Technologies Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
package com.networknt.schema.format;
218

319
import com.networknt.org.apache.commons.validator.routines.EmailValidator;
420
import com.networknt.schema.ExecutionContext;
21+
import com.networknt.schema.utils.Strings;
522

623
/**
724
* Format for idn-email.
@@ -19,6 +36,9 @@ public IdnEmailFormat(EmailValidator emailValidator) {
1936

2037
@Override
2138
public boolean matches(ExecutionContext executionContext, String value) {
39+
if (Strings.containsNonAsciiWhitespace(value)) {
40+
return false;
41+
}
2242
return this.emailValidator.isValid(value);
2343
}
2444

src/main/java/com/networknt/schema/utils/Strings.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,20 @@ public static String[] split(String text, char delimiter) {
169169

170170
return segments.toArray(new String[segments.size()]);
171171
}
172+
173+
/**
174+
* Whether the given string contains a non-ASCII whitespace character such as
175+
* {@code U+00A0} NON-BREAKING SPACE. Note that {@link Character#isWhitespace(int)}
176+
* deliberately excludes {@code U+00A0}, hence the additional
177+
* {@link Character#isSpaceChar(int)} check.
178+
*
179+
* @param string the string to test, may be null
180+
* @return true if the string contains a non-ASCII whitespace character
181+
*/
182+
public static boolean containsNonAsciiWhitespace(String string) {
183+
if (string == null) {
184+
return false;
185+
}
186+
return string.codePoints().anyMatch(ch -> ch > 0x7F && (Character.isWhitespace(ch) || Character.isSpaceChar(ch)));
187+
}
172188
}

src/test/java/com/networknt/schema/format/EmailFormatTest.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@
2121
import java.util.List;
2222

2323
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.params.ParameterizedTest;
25+
import org.junit.jupiter.params.provider.ValueSource;
2426

2527
import com.networknt.schema.Error;
2628
import com.networknt.schema.InputFormat;
2729
import com.networknt.schema.Schema;
2830
import com.networknt.schema.SchemaRegistry;
2931
import com.networknt.schema.SpecificationVersion;
32+
import com.networknt.schema.serialization.JsonMapperFactory;
3033

3134
class EmailFormatTest {
3235

@@ -35,13 +38,14 @@ class EmailFormatTest {
3538

3639
/**
3740
* Validates {@code email} against a {@code {"format": "email"}} schema with
38-
* format assertions turned on, and returns the validation errors.
41+
* format assertions turned on, and returns the validation errors. The email is
42+
* serialized via the mapper so a value containing quotes is escaped correctly.
3943
*/
4044
private List<Error> validateEmail(String email) {
4145
String schemaData = "{\r\n"
4246
+ " \"format\": \"email\"\r\n"
4347
+ "}";
44-
String inputData = "\"" + email + "\"";
48+
String inputData = JsonMapperFactory.getInstance().writeValueAsString(email);
4549
Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12).getSchema(schemaData);
4650
return schema.validate(inputData, InputFormat.JSON,
4751
executionContext -> executionContext.executionConfig(executionConfig -> executionConfig.formatAssertionsEnabled(true)));
@@ -54,6 +58,16 @@ void validEmailShouldPass() {
5458
assertTrue(messages.isEmpty(), "a plainly valid email should pass");
5559
}
5660

61+
/**
62+
* A quoted local part with an ASCII space ({@code "joe bloggs"@example.com}) is a
63+
* valid email, so the non-ASCII whitespace guard must not reject it.
64+
*/
65+
@Test
66+
void quotedLocalPartWithAsciiSpaceShouldPass() {
67+
List<Error> messages = validateEmail("\"joe bloggs\"@example.com");
68+
assertTrue(messages.isEmpty(), "a quoted local part with an ASCII space should stay valid");
69+
}
70+
5771
/**
5872
* Reproduces issue #1164: a leading non-breaking space (U+00A0) should make
5973
* the email invalid, just like a regular leading space does, but it is
@@ -64,4 +78,16 @@ void emailWithLeadingNbspShouldFail() {
6478
List<Error> messages = validateEmail(NBSP + "name@email.com");
6579
assertFalse(messages.isEmpty(), "email with a leading non-breaking space (U+00A0) should be invalid");
6680
}
81+
82+
/**
83+
* The fix generalizes to other non-ASCII whitespace such as U+2003 EM SPACE
84+
* and U+3000 IDEOGRAPHIC SPACE.
85+
*/
86+
@ParameterizedTest
87+
@ValueSource(ints = { 0x2003, 0x3000 })
88+
void emailWithLeadingUnicodeWhitespaceShouldFail(int codePoint) {
89+
String whitespace = new String(Character.toChars(codePoint));
90+
List<Error> messages = validateEmail(whitespace + "name@email.com");
91+
assertFalse(messages.isEmpty(), "email with a leading non-ASCII whitespace character should be invalid");
92+
}
6793
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright (c) 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.networknt.schema.format;
17+
18+
import static org.junit.jupiter.api.Assertions.assertFalse;
19+
import static org.junit.jupiter.api.Assertions.assertTrue;
20+
21+
import java.util.List;
22+
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.params.ParameterizedTest;
25+
import org.junit.jupiter.params.provider.ValueSource;
26+
27+
import com.networknt.schema.Error;
28+
import com.networknt.schema.InputFormat;
29+
import com.networknt.schema.Schema;
30+
import com.networknt.schema.SchemaRegistry;
31+
import com.networknt.schema.SpecificationVersion;
32+
import com.networknt.schema.serialization.JsonMapperFactory;
33+
34+
class IdnEmailFormatTest {
35+
36+
/** U+00A0 NON-BREAKING SPACE, built from its code point so no invisible character sits in the source. */
37+
private static final String NBSP = new String(Character.toChars(0x00A0));
38+
39+
/**
40+
* Validates {@code email} against a {@code {"format": "idn-email"}} schema with
41+
* format assertions turned on, and returns the validation errors. The email is
42+
* serialized via the mapper so a value containing quotes is escaped correctly.
43+
*/
44+
private List<Error> validateIdnEmail(String email) {
45+
String schemaData = "{\r\n"
46+
+ " \"format\": \"idn-email\"\r\n"
47+
+ "}";
48+
String inputData = JsonMapperFactory.getInstance().writeValueAsString(email);
49+
Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12).getSchema(schemaData);
50+
return schema.validate(inputData, InputFormat.JSON,
51+
executionContext -> executionContext.executionConfig(executionConfig -> executionConfig.formatAssertionsEnabled(true)));
52+
}
53+
54+
/** Sanity check that the test harness accepts a plainly valid idn-email. */
55+
@Test
56+
void validIdnEmailShouldPass() {
57+
List<Error> messages = validateIdnEmail("name@email.com");
58+
assertTrue(messages.isEmpty(), "a plainly valid idn-email should pass");
59+
}
60+
61+
/**
62+
* idn-email allows non-ASCII letters (unlike ASCII-only email), so the narrow
63+
* whitespace-only guard must not reject a non-ASCII letter in the local part.
64+
*/
65+
@Test
66+
void idnEmailWithNonAsciiLetterShouldPass() {
67+
// U+00FC LATIN SMALL LETTER U WITH DIAERESIS, built from its code point.
68+
String localPart = "m" + new String(Character.toChars(0x00FC)) + "nchen";
69+
List<Error> messages = validateIdnEmail(localPart + "@example.com");
70+
assertTrue(messages.isEmpty(), "idn-email should allow a non-ASCII letter in the local part");
71+
}
72+
73+
/**
74+
* idn-email shares EmailFormat's delegation, so a leading non-breaking space
75+
* (U+00A0) must be rejected here too.
76+
*/
77+
@Test
78+
void idnEmailWithLeadingNbspShouldFail() {
79+
List<Error> messages = validateIdnEmail(NBSP + "name@email.com");
80+
assertFalse(messages.isEmpty(), "idn-email with a leading non-breaking space (U+00A0) should be invalid");
81+
}
82+
83+
/**
84+
* The fix generalizes to other non-ASCII whitespace such as U+2003 EM SPACE
85+
* and U+3000 IDEOGRAPHIC SPACE, mirroring the equivalent EmailFormatTest case.
86+
*/
87+
@ParameterizedTest
88+
@ValueSource(ints = { 0x2003, 0x3000 })
89+
void idnEmailWithLeadingUnicodeWhitespaceShouldFail(int codePoint) {
90+
String whitespace = new String(Character.toChars(codePoint));
91+
List<Error> messages = validateIdnEmail(whitespace + "name@email.com");
92+
assertFalse(messages.isEmpty(), "idn-email with a leading non-ASCII whitespace character should be invalid");
93+
}
94+
}

0 commit comments

Comments
 (0)