Skip to content

Users: Avoid defaulting new user's display name, nickname, or nicename to an email address - #15400

Open
robelsust wants to merge 4 commits into
WordPress:trunkfrom
robelsust:fix/66446-user-email-display-name-privacy
Open

robelsust wants to merge 4 commits into
WordPress:trunkfrom
robelsust:fix/66446-user-email-display-name-privacy

Conversation

@robelsust

Copy link
Copy Markdown

Trac ticket: #66446

What

Prevents WordPress from automatically using an email address as a user's public display_name, nickname, or user_nicename when accounts are created with an email address as their username (user_login).

Why

Many sites and plugins (membership plugins, WooCommerce, SSO integrations, self-registration) register users whose user_login is formatted as an email address. Currently:

  • display_name: Falls back to $user_login (john@example.com) if first/last names are absent. This gets exposed in post bylines via get_the_author(), RSS/Atom feeds (<dc:creator>), oEmbed data, and the public REST API (/wp/v2/users).
  • nickname: Defaults to $user_login (john@example.com).
  • user_nicename: Generates sanitize_title( $user_login ) (johnexample-com), exposing the user's email domain and local-part in author archive URLs (/author/johnexample-com/), sitemaps, and comment CSS classes (comment-author-{nicename}).
  • user-edit.php & user-profile.js: Unconditionally offers user_login in the "Display name publicly as" select dropdown.

Because WordPress accounts allow authenticating with email addresses (since 4.5), exposing the email address as a public display name and URL slug presents both an account enumeration surface and a privacy violation (GDPR Art. 25(2) - data protection by default).

How

  1. wp_insert_user() in src/wp-includes/user.php:

    • display_name & nickname: When $user_login is an email address (is_email( $user_login )) and no explicit names are provided, default to a neutral string (__( 'User' )) instead of the email address. Explicit first/last names or explicit display_name inputs still take precedence.
    • user_nicename: When $user_login is an email address and no explicit nicename is passed, generate an unexposed random slug ('user-' . strtolower( wp_generate_password( 8, false, false ) )) rather than slugifying the email domain into the URL.
    • Update Preservation: When updating an existing user ($update = true) where no new user_nicename is provided in $userdata, preserve $old_user_data->user_nicename so existing accounts and author links are never altered.
    • DocBlocks: Updated @since 7.2.0 changelog and @type descriptions for $display_name, $nickname, and $user_nicename.
  2. src/wp-admin/user-edit.php:

    • Exclude $public_display['display_username'] from the "Display name publicly as" dropdown when is_email( $profile_user->user_login ) is true.
  3. src/js/_enqueues/admin/user-profile.js:

    • In the blur.user_profile event handler, avoid re-adding display_username if the username contains an @ / email address.
  4. Dedicated Unit Tests (tests/phpunit/tests/user/wpInsertUser.php):

    • Added canonical unit tests covering email login creation, explicit first/last name precedence, explicit overrides, unique nicename generation, preservation on updates, wp_create_user(), complex email addresses, and filter overrides (pre_user_display_name, pre_user_nickname, pre_user_nicename).

Testing Instructions

Automated Tests

Run PHPUnit tests across single-site and multisite:

# Single-site test suite
vendor/bin/phpunit tests/phpunit/tests/user/wpInsertUser.php

# Multisite test suite
vendor/bin/phpunit -c tests/phpunit/multisite.xml tests/phpunit/tests/user/wpInsertUser.php

# Code style check
vendor/bin/phpcs src/wp-admin/user-edit.php tests/phpunit/tests/user/wpInsertUser.php

Expected output:

  • OK (11 tests, 42 assertions) on both single-site and multisite.
  • 0 errors, 0 warnings on PHPCS.

Manual Testing

  1. Navigate to Users > Add New (or use wp_create_user( 'member@example.com', 'password', 'member@example.com' )).
  2. Create a user with Username = member@example.com and Email = member@example.com without providing First Name or Last Name.
  3. Inspect the newly created user in Users > All Users and Edit User:
    • The user's Display name publicly as should show User, not member@example.com.
    • The user's author archive URL should be /author/user-{random}/, not /author/memberexample-com/.
    • Edit the user's First Name and tab out: the dropdown updates to include the new name without re-injecting the email address.
  4. Create a user with a standard username (e.g. regularuser):
    • Verify that standard usernames continue to default display_name, nickname, and user_nicename to regularuser as before.

…e to an email address.

When an account is created with an email address as the username (user_login), defaulting display_name, nickname, or user_nicename to that username exposes the user's email address publicly in post bylines, feeds, comment markup, REST API responses, and author archive URLs.

This changeset updates user handling:
* In wp_insert_user(), when $user_login is an email address and no explicit names are supplied, display_name and nickname fall back to a neutral string ('User') instead of the email address.
* In wp_insert_user(), user_nicename is generated as an unexposed random slug (user-{random}) rather than slugifying the email address.
* When updating an existing user with an email username and no user_nicename is passed, the existing user_nicename is preserved.
* In user-edit.php and user-profile.js, display_username is excluded from the "Display name publicly as" options when the username is an email address.
* Adds dedicated unit tests in tests/phpunit/tests/user/wpInsertUser.php covering new user creation, complex email addresses, explicit name overrides, existing user updates, and filter interactions.

Props therssoftware.
Fixes #66446.
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs.

Core Committers: Use this line as a base for the props when committing in SVN:

Props therssoftware, dpknauss.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@dknauss

dknauss commented Nov 3, 2026

Copy link
Copy Markdown

Thanks for the patch! I tested it locally on single-site and multisite, and the account-default changes passed.

I left you feedback on Trac suggesting the profile dropdown changes should not go into this initial fix: https://core.trac.wordpress.org/ticket/66446

Restores user-edit.php and user-profile.js so the "Display name publicly as" dropdown remains unchanged, focusing this changeset strictly on account defaults for newly registered users in wp_insert_user().

Props therssoftware, dpknauss.
See #66446.
@dknauss

dknauss commented Nov 9, 2026

Copy link
Copy Markdown

Some review observations:

The patch covers all three parts of the proposal on Trac #66446: no email fallback for display_name/nickname, a random user_nicename, and dropping the login from "Display name publicly as". Because REST name/slug, bylines, feeds, oEmbed and comment_author all read display_name or user_nicename, they're fixed downstream too.

A few notes:

On the neutral default

  1. Every email-login account becomes "User". All such accounts share one byline and one comment name, so moderators, readers and author dropdowns (wp_dropdown_users() shows display_name) can't tell them apart. Could the default be unique but still anonymous, for example "User" plus the random part of the generated nicename (User k3j9x2ab)? The user ID isn't a good substitute: it isn't known before insert, and a sequential number leaks account counts.
  2. __( 'User' ) is stored in the locale active at registration. A site can end up with "User", "Utilisateur" and "Benutzer" side by side, and changing the site language never updates them. Is that acceptable, or should the stored default avoid translated text?

Smaller notes

  1. The new "preserve existing nicename on update" branch applies to all users, not only email logins (a direct wp_insert_user() call with an ID and a new user_login no longer rebuilds the slug). wp_update_user() merges existing data first, so most callers won't notice, but it may be better as its own change or with a test for the non-email case.
  2. wp_generate_password() runs the random_password filter, so a plugin that filters passwords also controls author slugs. bin2hex( random_bytes( 4 ) ) or a slice of wp_generate_uuid4() avoids that, and the function_exists() guard probably isn't needed.
  3. PHP uses is_email() while user-profile.js checks for @. A login like a@b (allowed by sanitize_user(), rejected by is_email()) is listed on page load and then removed when the JS rebuilds the list.

Test gaps

  • Nothing asserts the downstream privacy outcome: REST name/slug, comment_author on a logged-in comment, feed author, author URL.
  • No test for the profile screen options (PHP or JS).
  • No multisite path (wpmu_create_user()).
  • No test that an existing account whose display_name already is its email stays unchanged after wp_update_user().

…lose test gaps.

Updates wp_insert_user() to use bin2hex( random_bytes( 4 ) ) for generating
an unexposed, unique random suffix for email logins ('User {suffix}' and
'user-{suffix}'). Scopes nicename preservation on update strictly to email logins.
Aligns user-edit.php and user-profile.js via data-is-email attribute.
Adds tests for downstream privacy, multisite, legacy account updates,
and profile options.

Props therssoftware, dpknauss.
See #66446.
@robelsust

Copy link
Copy Markdown
Author

Thanks for the thorough and insightful review, @dknauss!

I've pushed updates addressing all of your observations:

  1. Unique Anonymous Default:

    • Default display_name and nickname now use 'User ' . $suffix, and user_nicename uses 'user-' . $suffix, where $suffix = bin2hex( random_bytes( 4 ) ) (e.g., User 3a9f02c1 and user-3a9f02c1).
    • This ensures author dropdowns (wp_dropdown_users()), bylines, and comment listings remain uniquely distinguishable while preserving privacy, without leaking incremental account counts.
    • Removed the localized __( 'User' ) wrapper so stored database defaults don't create mixed-language drift across registrations.
  2. Slug Generation:

    • Replaced wp_generate_password() with bin2hex( random_bytes( 4 ) ), ensuring plugin filters on random_password cannot alter author slugs or display names.
  3. Nicename Preservation on Update:

    • Scoped the preservation check during $update strictly to email-login accounts ($update && is_email( $user_login ) && ! empty( $old_user_data->user_nicename )), leaving standard username updates untouched.
  4. Profile Screen Alignment (PHP & JS):

    • In user-edit.php, #user_login now receives data-is-email="true" via is_email(), and user-profile.js checks this attribute before adding display_username to the public display options. This aligns PHP and JS behavior for usernames containing @.
  5. Downstream & Edge-Case Test Coverage:

    • Added downstream privacy tests covering post bylines (get_the_author()), author URLs (get_author_posts_url()), authenticated comment author submission (wp_handle_comment_submission()), and the REST API (/wp/v2/users/{id}).
    • Added a Multisite test for wpmu_create_user().
    • Added a test verifying wp_update_user() does not overwrite legacy accounts whose display name was already an email.
    • Added a test verifying standard username nicenames remain intact on update.
    • Added tests covering public display options on the profile screen.

@dknauss dknauss left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the update. I tested 53264a2 on disposable single-site and multisite installs using PHP 8.4 and SQLite. New accounts created through wp_create_user() and wpmu_create_user() got matching anonymous display-name, nickname, and slug suffixes. Public REST names, REST slugs, and author URLs reflected the anonymous defaults. wp_update_user() preserved an explicitly saved legacy email display name.

I found one update regression in both installs. Two calls to wp_insert_user() with the same existing ID and login, changing only the description and leaving the public name fields out, generated two different display names and nicknames. The author slug stayed fixed. An integration that calls wp_insert_user() directly can therefore change someone’s public byline every time it syncs their profile. Could we preserve the stored display name and nickname when those keys are omitted on update and add coverage for that case? The current legacy test exercises wp_update_user(), which merges stored fields before calling wp_insert_user().

One test gap remains on the profile screen. The new test builds its own array by repeating the condition from user-edit.php. It would still pass if the dropdown or its JS blur handler offered the email login. Could the test exercise the actual profile dropdown, both on initial render and after editing the nickname?

AI assistance: Yes. OpenAI Codex ran the PHP fixtures, inspected the patch, and drafted this review with me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants