From e0880191091d0af639b20f619f4df13b80b890a1 Mon Sep 17 00:00:00 2001 From: Iker Pedrosa Date: Tue, 2 Jun 2026 13:15:42 +0200 Subject: [PATCH 1/3] lib/chkname.*: Add UPN validation support Add is_valid_upn() function to validate User Principal Name format. UPN validation splits on @ and validates the prefix using existing username rules and suffix part using RFC 1035/1123 compliant domain name validation. Link: Co-authored-by: Iker Pedrosa Signed-off-by: Iker Pedrosa Co-authored-by: Alejandro Colomar Signed-off-by: Alejandro Colomar Reviewed-by: Alejandro Colomar --- lib/chkname.c | 92 +++++++++++++++++++++++++++++++++++++++++++++++++-- lib/chkname.h | 1 + 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/lib/chkname.c b/lib/chkname.c index 1ef33ad3eb..ee8ff9d108 100644 --- a/lib/chkname.c +++ b/lib/chkname.c @@ -7,8 +7,7 @@ /* - * is_valid_user_name(), is_valid_group_name() - check the new user/group - * name for validity; + * check user/group names for valid syntax * return values: * true - OK * false - bad name @@ -33,12 +32,19 @@ #include "defines.h" #include "chkname.h" +#include "sizeof.h" #include "string/ctype/isascii.h" #include "string/strcmp/streq.h" #include "string/strcmp/strcaseeq.h" +#include "string/strspn/strrcspn.h" +#include "string/strtok/stpsep.h" #include "sysconf.h" +#define DOMAIN_MAXLEN 255 +#define LABEL_MAXLEN 63 + + int allow_bad_names = false; @@ -126,3 +132,85 @@ is_valid_group_name(const char *name) return is_valid_name (name); } + + +// Validate a single DNS domain label according to RFC 1035 2.3.1. +static bool +is_valid_domain_label(const char *label) +{ + if (strlen(label) > LABEL_MAXLEN) { + errno = EOVERFLOW; + return false; + } + if (!strspn(label, CTYPE_ALPHA_C) || !strrcspn(label, "-")) { + errno = EINVAL; + return false; + } + if (!streq(stpspn(label, CTYPE_ALNUM_C "-"), "")) { + errno = EINVAL; + return false; + } + + return true; +} + + +/* + * Validate a domain name according to RFC 1035 by splitting + * it into labels and validating each label individually. + */ +static bool +is_valid_domain_name(const char *domain) +{ + char *d; + const char *l; + + if (!strcspn(domain, ".")) { + errno = EINVAL; + return false; + } + + if (strlen(domain) + !!strrcspn(domain, ".") > DOMAIN_MAXLEN) { + errno = EOVERFLOW; + return false; + } + d = strdupa(domain); + + while (NULL != (l = strsep(&d, "."))) { + if (d == NULL && streq(l, "")) // trailing root label + break; + if (!is_valid_domain_label(l)) + return false; + } + + return true; +} + + +/* + * is_valid_upn - is valid User Principal Name + * + * Check UPN format (user@domain) for validity. + * + * This function only validates syntax, not whether the UPN exists + * in any authentication system. + */ +bool +is_valid_upn(const char *upn) +{ + char *u, *d; + + if (strlen(upn) >= LOGIN_NAME_MAX + STRLEN("@") + DOMAIN_MAXLEN) { + errno = EOVERFLOW; + return false; + } + u = strdupa(upn); + + d = stpsep(u, "@"); + if (d == NULL) { + errno = EINVAL; + return false; + } + + return is_valid_user_name(u) && is_valid_domain_name(d); +} diff --git a/lib/chkname.h b/lib/chkname.h index 1f8e4e0822..de93b8251a 100644 --- a/lib/chkname.h +++ b/lib/chkname.h @@ -28,5 +28,6 @@ extern bool is_valid_user_name (const char *name); extern bool is_valid_group_name (const char *name); +extern bool is_valid_upn (const char *name); #endif From 2bb9f20071f09faf8d101553077f52b2cac7e7b5 Mon Sep 17 00:00:00 2001 From: Iker Pedrosa Date: Tue, 2 Jun 2026 15:01:36 +0200 Subject: [PATCH 2/3] tests: add unit tests for UPN validation Add comprehensive unit tests for is_valid_upn() function in `tests/unit/test_chkname.c` covering: - Valid UPN formats (user@domain.com) - Invalid structures (missing @, multiple @) - Domain validation (RFC compliance) - Boundary limits (253/254 char domains, 63+ char labels) Signed-off-by: Iker Pedrosa Reviewed-by: Alejandro Colomar --- tests/unit/test_chkname.c | 131 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/tests/unit/test_chkname.c b/tests/unit/test_chkname.c index 842337b9f5..a211b9f593 100644 --- a/tests/unit/test_chkname.c +++ b/tests/unit/test_chkname.c @@ -30,6 +30,12 @@ static void test_is_valid_user_name_nok_empty(MAYBE_UNUSED void ** _1); static void test_is_valid_user_name_nok_numeric(MAYBE_UNUSED void ** _1); static void test_is_valid_user_name_nok_otherchars(MAYBE_UNUSED void ** _1); static void test_is_valid_user_name_long(MAYBE_UNUSED void ** _1); +static void test_is_valid_upn_ok(MAYBE_UNUSED void ** _1); +static void test_is_valid_upn_nok_not_upn(MAYBE_UNUSED void ** _1); +static void test_is_valid_upn_nok_structure(MAYBE_UNUSED void ** _1); +static void test_is_valid_upn_nok_domain(MAYBE_UNUSED void ** _1); +static void test_is_valid_upn_ok_limits(MAYBE_UNUSED void ** _1); +static void test_is_valid_upn_nok_limits(MAYBE_UNUSED void ** _1); int @@ -45,6 +51,12 @@ main(void) cmocka_unit_test(test_is_valid_user_name_nok_numeric), cmocka_unit_test(test_is_valid_user_name_nok_otherchars), cmocka_unit_test(test_is_valid_user_name_long), + cmocka_unit_test(test_is_valid_upn_ok), + cmocka_unit_test(test_is_valid_upn_nok_not_upn), + cmocka_unit_test(test_is_valid_upn_nok_structure), + cmocka_unit_test(test_is_valid_upn_nok_domain), + cmocka_unit_test(test_is_valid_upn_ok_limits), + cmocka_unit_test(test_is_valid_upn_nok_limits), }; return cmocka_run_group_tests(tests, NULL, NULL); @@ -146,3 +158,122 @@ test_is_valid_user_name_long(MAYBE_UNUSED void ** _1) free(name); } + + +static void +test_is_valid_upn_ok(MAYBE_UNUSED void ** _1) +{ + assert_true(is_valid_upn("user@example.com")); + assert_true(is_valid_upn("john.doe@corp.example.org")); + assert_true(is_valid_upn("test@sub.domain.net")); + assert_true(is_valid_upn("a@b.c")); + assert_true(is_valid_upn("user123@test123.example")); + assert_true(is_valid_upn("user_name@example-domain.com")); + assert_true(is_valid_upn("test.user@example.domain.org")); + assert_true(is_valid_upn("user@domain")); + assert_true(is_valid_upn("user@domain.com.")); + assert_true(is_valid_upn("user@sub.")); +} + + +static void +test_is_valid_upn_nok_not_upn(MAYBE_UNUSED void ** _1) +{ + assert_true(false == is_valid_upn("regularuser")); + assert_true(false == is_valid_upn("user.name")); + assert_true(false == is_valid_upn("user_name")); + assert_true(false == is_valid_upn("123user")); + assert_true(false == is_valid_upn("USER")); +} + + +static void +test_is_valid_upn_nok_structure(MAYBE_UNUSED void ** _1) +{ + // Empty parts + assert_true(false == is_valid_upn("@domain.com")); + assert_true(false == is_valid_upn("user@")); + assert_true(false == is_valid_upn("@")); + + // Multiple @ symbols + assert_true(false == is_valid_upn("user@domain@com")); + assert_true(false == is_valid_upn("@@domain.com")); + assert_true(false == is_valid_upn("user@@domain.com")); + + // Empty string + assert_true(false == is_valid_upn("")); + + // Invalid prefix + assert_true(false == is_valid_upn("-user@domain.com")); + assert_true(false == is_valid_upn("123@domain.com")); + assert_true(false == is_valid_upn("user space@domain.com")); +} + + +static void +test_is_valid_upn_nok_domain(MAYBE_UNUSED void ** _1) +{ + // Invalid domain formats + assert_true(false == is_valid_upn("user@.domain.com")); + assert_true(false == is_valid_upn("user@domain..com")); + + // Invalid domain characters + assert_true(false == is_valid_upn("user@domain_name.com")); + assert_true(false == is_valid_upn("user@domain name.com")); +} + + +static void +test_is_valid_upn_ok_limits(MAYBE_UNUSED void ** _1) +{ + char *upn; + char *domain; + + // Test domain at maximum allowed length (254 chars + implicit root = 255) + domain = malloc_T(254 + 1, char); + assert_true(domain != NULL); + strcpy(domain, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa." // 63 a's + dot + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb." // 63 b's + dot + "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc." // 63 c's + dot + "ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd." // 59 d's + dot + "co"); // 2 + // Total: 63 + 1 + 63 + 1 + 63 + 1 + 59 + 1 + 2 (+ 1 implicit root) = 255 chars + + upn = malloc_T(5 + 254 + 1, char); // "user@" + domain + '\0' + assert_true(upn != NULL); + strcpy(upn, "user@"); + strcat(upn, domain); + assert_true(is_valid_upn(upn)); + + free(upn); + free(domain); +} + + +static void +test_is_valid_upn_nok_limits(MAYBE_UNUSED void ** _1) +{ + char *upn; + char *domain; + + // Test domain exceeding maximum length (255 chars + implicit root = 256 > 255 limit) + domain = malloc_T(255 + 1, char); + assert_true(domain != NULL); + memset(domain, 'a', 252); + strcpy(&domain[252], ".co"); + + upn = malloc_T(5 + 255 + 1, char); // "user@" + domain + '\0' + assert_true(upn != NULL); + strcpy(upn, "user@"); + strcat(upn, domain); + assert_true(false == is_valid_upn(upn)); + + free(upn); + free(domain); + + // Domain label too long (>63 chars) + assert_true(false == is_valid_upn("user@" + "verylongdomainlabelthatexceedssixtythreecharacterslimitsetbyRFC1035" + ".com")); +} From d012634c7a2110f4d01931f993680c476c6cc027 Mon Sep 17 00:00:00 2001 From: Iker Pedrosa Date: Tue, 2 Jun 2026 15:18:31 +0200 Subject: [PATCH 3/3] passwd: add UPN validation support Add User Principal Name (UPN) validation to allow passwd command to accept usernames in user@domain.com format. Currently, passwd will accept both traditional usernames and UPN format. Fixes: 326889ca (2024-10-22; "Fix coverity unbound buffer issues") Closes: Reported-by: @nooreldeenmansour Signed-off-by: Iker Pedrosa Reviewed-by: Alejandro Colomar --- src/passwd.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/passwd.c b/src/passwd.c index b2cac80b00..351252e0b5 100644 --- a/src/passwd.c +++ b/src/passwd.c @@ -993,7 +993,7 @@ main(int argc, char **argv) } myname = xstrdup (pw->pw_name); if (optind < argc) { - if (!is_valid_user_name (argv[optind])) { + if (!is_valid_user_name (argv[optind]) && !is_valid_upn (argv[optind])) { fprintf (stderr, _("%s: Provided user name is not a valid name\n"), Prog); fail_exit (E_NOPERM, process_selinux); }