Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 2 additions & 16 deletions src/main/java/com/networknt/schema/format/EmailFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import com.networknt.org.apache.commons.validator.routines.EmailValidator;
import com.networknt.schema.ExecutionContext;
import com.networknt.schema.utils.Strings;

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

@Override
public boolean matches(ExecutionContext executionContext, String value) {
if (containsNonAsciiWhitespace(value)) {
if (Strings.containsNonAsciiWhitespace(value)) {
return false;
}
return this.emailValidator.isValid(value);
}

/**
* Whether {@code value} contains a non-ASCII whitespace character such as
* {@code U+00A0} NON-BREAKING SPACE. The underlying validator only excludes
* ASCII whitespace, so such characters would otherwise be accepted in an
* email address. Note that {@link Character#isWhitespace(char)} deliberately
* excludes {@code U+00A0}, hence the additional
* {@link Character#isSpaceChar(char)} check.
*/
private static boolean containsNonAsciiWhitespace(String value) {
if (value == null) {
return false;
}
return value.codePoints().anyMatch(ch -> ch > 0x7F && (Character.isWhitespace(ch) || Character.isSpaceChar(ch)));
}

@Override
public String getName() {
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/networknt/schema/format/IdnEmailFormat.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
/*
* Copyright (c) 2016 Network New Technologies Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.networknt.schema.format;

import com.networknt.org.apache.commons.validator.routines.EmailValidator;
import com.networknt.schema.ExecutionContext;
import com.networknt.schema.utils.Strings;

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

@Override
public boolean matches(ExecutionContext executionContext, String value) {
if (Strings.containsNonAsciiWhitespace(value)) {
return false;
}
return this.emailValidator.isValid(value);
}

Expand Down
16 changes: 16 additions & 0 deletions src/main/java/com/networknt/schema/utils/Strings.java
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,20 @@ public static String[] split(String text, char delimiter) {

return segments.toArray(new String[segments.size()]);
}

/**
* Whether the given string contains a non-ASCII whitespace character such as
* {@code U+00A0} NON-BREAKING SPACE. Note that {@link Character#isWhitespace(int)}
* deliberately excludes {@code U+00A0}, hence the additional
* {@link Character#isSpaceChar(int)} check.
*
* @param string the string to test, may be null
* @return true if the string contains a non-ASCII whitespace character
*/
public static boolean containsNonAsciiWhitespace(String string) {
if (string == null) {
return false;
}
return string.codePoints().anyMatch(ch -> ch > 0x7F && (Character.isWhitespace(ch) || Character.isSpaceChar(ch)));
}
}
30 changes: 28 additions & 2 deletions src/test/java/com/networknt/schema/format/EmailFormatTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@
import java.util.List;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import com.networknt.schema.Error;
import com.networknt.schema.InputFormat;
import com.networknt.schema.Schema;
import com.networknt.schema.SchemaRegistry;
import com.networknt.schema.SpecificationVersion;
import com.networknt.schema.serialization.JsonMapperFactory;

class EmailFormatTest {

Expand All @@ -35,13 +38,14 @@ class EmailFormatTest {

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

/**
* A quoted local part with an ASCII space ({@code "joe bloggs"@example.com}) is a
* valid email, so the non-ASCII whitespace guard must not reject it.
*/
@Test
void quotedLocalPartWithAsciiSpaceShouldPass() {
List<Error> messages = validateEmail("\"joe bloggs\"@example.com");
assertTrue(messages.isEmpty(), "a quoted local part with an ASCII space should stay valid");
}

/**
* Reproduces issue #1164: a leading non-breaking space (U+00A0) should make
* the email invalid, just like a regular leading space does, but it is
Expand All @@ -64,4 +78,16 @@ void emailWithLeadingNbspShouldFail() {
List<Error> messages = validateEmail(NBSP + "name@email.com");
assertFalse(messages.isEmpty(), "email with a leading non-breaking space (U+00A0) should be invalid");
}

/**
* The fix generalizes to other non-ASCII whitespace such as U+2003 EM SPACE
* and U+3000 IDEOGRAPHIC SPACE.
*/
@ParameterizedTest
@ValueSource(ints = { 0x2003, 0x3000 })
void emailWithLeadingUnicodeWhitespaceShouldFail(int codePoint) {
String whitespace = new String(Character.toChars(codePoint));
List<Error> messages = validateEmail(whitespace + "name@email.com");
assertFalse(messages.isEmpty(), "email with a leading non-ASCII whitespace character should be invalid");
}
}
94 changes: 94 additions & 0 deletions src/test/java/com/networknt/schema/format/IdnEmailFormatTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright (c) 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.networknt.schema.format;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import com.networknt.schema.Error;
import com.networknt.schema.InputFormat;
import com.networknt.schema.Schema;
import com.networknt.schema.SchemaRegistry;
import com.networknt.schema.SpecificationVersion;
import com.networknt.schema.serialization.JsonMapperFactory;

class IdnEmailFormatTest {

/** U+00A0 NON-BREAKING SPACE, built from its code point so no invisible character sits in the source. */
private static final String NBSP = new String(Character.toChars(0x00A0));

/**
* Validates {@code email} against a {@code {"format": "idn-email"}} schema with
* format assertions turned on, and returns the validation errors. The email is
* serialized via the mapper so a value containing quotes is escaped correctly.
*/
private List<Error> validateIdnEmail(String email) {
String schemaData = "{\r\n"
+ " \"format\": \"idn-email\"\r\n"
+ "}";
String inputData = JsonMapperFactory.getInstance().writeValueAsString(email);
Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12).getSchema(schemaData);
return schema.validate(inputData, InputFormat.JSON,
executionContext -> executionContext.executionConfig(executionConfig -> executionConfig.formatAssertionsEnabled(true)));
}

/** Sanity check that the test harness accepts a plainly valid idn-email. */
@Test
void validIdnEmailShouldPass() {
List<Error> messages = validateIdnEmail("name@email.com");
assertTrue(messages.isEmpty(), "a plainly valid idn-email should pass");
}

/**
* idn-email allows non-ASCII letters (unlike ASCII-only email), so the narrow
* whitespace-only guard must not reject a non-ASCII letter in the local part.
*/
@Test
void idnEmailWithNonAsciiLetterShouldPass() {
// U+00FC LATIN SMALL LETTER U WITH DIAERESIS, built from its code point.
String localPart = "m" + new String(Character.toChars(0x00FC)) + "nchen";
List<Error> messages = validateIdnEmail(localPart + "@example.com");
assertTrue(messages.isEmpty(), "idn-email should allow a non-ASCII letter in the local part");
}

/**
* idn-email shares EmailFormat's delegation, so a leading non-breaking space
* (U+00A0) must be rejected here too.
*/
@Test
void idnEmailWithLeadingNbspShouldFail() {
List<Error> messages = validateIdnEmail(NBSP + "name@email.com");
assertFalse(messages.isEmpty(), "idn-email with a leading non-breaking space (U+00A0) should be invalid");
}

/**
* The fix generalizes to other non-ASCII whitespace such as U+2003 EM SPACE
* and U+3000 IDEOGRAPHIC SPACE, mirroring the equivalent EmailFormatTest case.
*/
@ParameterizedTest
@ValueSource(ints = { 0x2003, 0x3000 })
void idnEmailWithLeadingUnicodeWhitespaceShouldFail(int codePoint) {
String whitespace = new String(Character.toChars(codePoint));
List<Error> messages = validateIdnEmail(whitespace + "name@email.com");
assertFalse(messages.isEmpty(), "idn-email with a leading non-ASCII whitespace character should be invalid");
}
}
Loading