From a8c3632538b3946ed16be05a1a7a067439de30de Mon Sep 17 00:00:00 2001 From: Samuel Moelius Date: Wed, 16 Sep 2026 11:22:34 -0400 Subject: [PATCH] fix(isFQDN): reject unpaired UTF-16 surrogates --- src/lib/isFQDN.js | 6 ++++++ test/validators.test.js | 23 ++++++++++++++++++++--- test/validators/isFQDN.test.js | 22 ++++++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/lib/isFQDN.js b/src/lib/isFQDN.js index eb6928fda..a55855b5c 100644 --- a/src/lib/isFQDN.js +++ b/src/lib/isFQDN.js @@ -12,6 +12,12 @@ const default_fqdn_options = { export default function isFQDN(str, options) { assertString(str); + + // Reject unpaired surrogates while preserving valid surrogate pairs. + if (/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(str)) { + return false; + } + options = merge(options, default_fqdn_options); /* Remove the optional trailing dot before checking validity */ diff --git a/test/validators.test.js b/test/validators.test.js index 98d2a12ff..fa36e9f04 100644 --- a/test/validators.test.js +++ b/test/validators.test.js @@ -5785,9 +5785,26 @@ describe('Validators', () => { }); it('should reject emails containing unpaired UTF-16 surrogates without throwing', () => { - test({ - validator: 'isEmail', - invalid: ['\uD800@example.com', '\uDC00@example.com'], + [undefined, { ignore_max_length: true }].forEach((options) => { + test({ + validator: 'isEmail', + args: [options], + valid: ['user@example.com', 'user@é.com', 'user@\uD83D\uDE00.com', 'user@a\uD83D\uDE00b.com'], + invalid: [ + '\uD800@example.com', + '\uDC00@example.com', + 'user@\uD800.com', + 'user@\uDC00.com', + 'user@\uD800abc.com', + 'user@\uDC00abc.com', + 'user@a\uD800b.com', + 'user@a\uDC00b.com', + 'user@abc\uD800.com', + 'user@abc\uDC00.com', + 'user@\uD83D\uDE00\uDC00.com', + 'user@\uD800\uD83D\uDE00.com', + ], + }); }); }); diff --git a/test/validators/isFQDN.test.js b/test/validators/isFQDN.test.js index 134bab005..236fd37ba 100644 --- a/test/validators/isFQDN.test.js +++ b/test/validators/isFQDN.test.js @@ -1,6 +1,28 @@ import test from '../testFunctions'; describe('isFQDN', () => { + it('should reject unpaired UTF-16 surrogates without throwing', () => { + [undefined, { ignore_max_length: true }].forEach((options) => { + test({ + validator: 'isFQDN', + args: [options], + valid: ['example.com', 'é.com', '\uD83D\uDE00.com', 'a\uD83D\uDE00b.com'], + invalid: [ + '\uD800.com', + '\uDC00.com', + '\uD800abc.com', + '\uDC00abc.com', + 'a\uD800b.com', + 'a\uDC00b.com', + 'abc\uD800.com', + 'abc\uDC00.com', + '\uD83D\uDE00\uDC00.com', + '\uD800\uD83D\uDE00.com', + ], + }); + }); + }); + it('should validate domain names.', () => { test({ validator: 'isFQDN',