8000 Validate group names for control characters by Inf1mum · Pull Request #54828 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

Validate group names for control characters - #54828

Open
Inf1mum wants to merge 1 commit into
keycloak:mainfrom
Inf1mum:fix/54721-group-name-control-chars
Open

Inf1mum wants to merge 1 commit into
keycloak:mainfrom
Inf1mum:fix/54721-group-name-control-chars

Conversation

@Inf1mum
@Inf1mum Inf1mum commented Nov 6, 2026
Copy link
Copy Markdown
Contributor

Closes #54721

What

Group names submitted via the Admin REST API are now checked for control characters (Unicode general category Cc): NUL (U+0000), the other C0 controls, DEL (U+007F) and the C1 controls (U+0080-U+009F). Requests that create or rename a group with such a name are rejected with 400 Bad Request and the message Group name contains control characters.

Why

As described in #54721, such characters are accepted today when submitted as JSON escapes (a raw NUL byte is rejected by the JSON parser, but the escaped form passes through). They are stored verbatim and round-trip through the Admin API:

  1. On PostgreSQL deployments, any later operation that reads or updates the affected row fails at the JDBC layer, because PostgreSQL does not allow NUL bytes in text values - the same failure mode as #53897 (NUL in WebAuthn user handles).
  2. The control character propagates into group path claims in tokens and is invisible in the Admin UI, silently breaking exact string matching.

Where

  • New utility Validation.containsControlCharacters(...) - checks every code point against Character.CONTROL (Unicode category Cc)
  • POST /admin/realms/{realm}/groups (top-level create - validated only when creating; requests that move an existing group are unaffected)
  • POST /admin/realms/{realm}/groups/{id}/children (subgroup create - same creation-only scope)
  • PUT /admin/realms/{realm}/groups/{id} (rename - checked only when the name actually changes)

Note: PR #54719 addresses the same family of issues for invisible formatting characters (category Cf, tracked in #54532). This PR is independent of it and can be merged in either order; if both are applied, the checks complement each other.

Backward compatibility / trade-offs

  • Only new writes are validated. Existing groups whose stored names already contain control characters are untouched: updates that leave the name unchanged are still accepted (covered by updatingLegacyGroupWithControlCharactersInNameSucceeds), and moving such a group via the move endpoints is also allowed, since those requests do not apply the name (covered by moveLegacyGroupWithControlCharactersInNameSucceeds).
  • Names with visible special characters (accents, CJK, emoji, ...) remain valid - only category Cc is rejected (covered by createGroupWithVisibleSpecialCharactersNameSucceeds).
  • Realm import and organization-managed group writes are not covered by this change.

Testing

  • 6 new tests in GroupTest:
    • createGroupWithControlCharacterNameShouldFail - NUL, DEL and a C1 control on the create path, including the error message
    • renamingGroupToControlCharacterNameShouldFail - rename path
    • createSubGroupWithControlCharacterNameShouldFail - subgroup path
    • createGroupWithVisibleSpecialCharactersNameSucceeds - no over-blocking of visible characters
    • updatingLegacyGroupWithControlCharactersInNameSucceeds - legacy names can still be managed without renaming
    • moveLegacyGroupWithControlCharactersInNameSucceeds - move endpoints do not validate unchanged legacy names
  • Full GroupTest class passes locally (40 tests, 0 failures).

Group names submitted via the Admin REST API could contain control
characters (Unicode general category Cc, e.g. NUL) when submitted as
JSON escapes. Such names were stored verbatim and round-tripped through
the API, which breaks PostgreSQL deployments (NUL bytes are not allowed
in text values) and silently breaks exact string matching as the
characters are invisible in the Admin UI and propagate into token claims.

Group create (top-level and sub-group) and rename requests are now
rejected with 400 Bad Request when the name contains a control
character. Names of existing (legacy) groups are only validated when
actually changed, and moving a group does not validate its name.

Closes keycloak#54721
@Inf1mum
Inf1mum requested a review from a team as a code owner November 6, 2026 20:43
Copilot AI balanced review requested due to automatic review settings November 6, 2026 20:43
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Both legacy-name tests fail during setup on PostgreSQL because NUL-containing group names are rejected on flush, preventing the update and move checks from running.

1 open issue
What changed in this PR

This PR addresses #54721 by rejecting control characters in group names submitted through the Admin REST API, while preserving updates and moves that leave existing names unchanged.

Changes:

  • Adds a shared Unicode category Cc validation helper.
  • Validates names during group creation, subgroup creation, and renaming.
  • Adds six tests covering rejection, visible Unicode names, and legacy-name compatibility.
File Description
tests/​base/​src/​test/​java/​org/​keycloak/​tests/​admin/​group/​GroupTest.java Adds validation and backward-compatibility tests.
services/​src/​main/​java/​org/​keycloak/​services/​validation/​Validation.java Adds control-character detection.
services/​src/​main/​java/​org/​keycloak/​services/​resources/​admin/​GroupsResource.java Validates new top-level group names without blocking moves.
services/​src/​main/​java/​org/​keycloak/​services/​resources/​admin/​GroupResource.java Validates renames and new subgroup names.

Review effort: Balanced

Comment on lines +464 to +473
GroupModel legacyGroup = realm.createGroup("legacy\u0000Group");
return legacyGroup.getId();
}, String.class);
managedRealm.cleanup().add(r -> r.groups().group(groupId).remove());

GroupRepresentation rep = new GroupRepresentation();
rep.setName("legacy\u0000Group");
managedRealm.admin().groups().group(groupId).update(rep);

assertEquals("legacy\u0000Group", managedRealm.admin().groups().group(groupId).toRepresentation().getName());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Group names can contain control characters (e.g. NUL) - silently stored, breaks PostgreSQL deployments and exact matching

2 participants

0