Preface

On the 2nd of September, we reported a security issue to the security team. We identified that usernames are being leaked by JSON API, which we consider a personal information disclosure, not just username enumeration. The security team reacted quickly, but they said it is not an issue (as per Disclosure of usernames and user IDs is not considered a weakness) and we can go public with this problem. We truly believe that the problem is real, so this is a follow-up on that security report and also a follow-up on List of user accounts by get /user/user endpoint from 2o19, which was about the same problem with similar concerns.

Intro

(The original intro was moved to a new policy type issue as @cilefen suggested: #3241232)

Problem/Motivation

As we identified since the original security report, the actual problem is not with the JSON API module. The JSON API module just provides one presentation layer in Drupal core where the actual problem is perfectly demonstrated. If the admin/people page would be available to everyone without "administer users" permission, the same information would be exposed to that to all visitors.

The actual problem: the label of an entity is always considered public in Drupal core, the user entity's label is its username, precisely the display name of a user is by default its username, but that can be changed via a hook_user_format_name_alter() implementation (another way to expose personal data accidentally).

History:
* Anonymous user label can't be viewed and auth user labels are only accessible with 'access user profiles' permission
* JSON API now provides only the label of inaccessible entities ('view' access denied) when 'view label' access is allowed
* Show label of inaccessible entities ('view' access denied) when 'view label' access is allowed

Steps to reproduce

1. Enable the JSON API module
2. Register or create a user, use an email address as username for example, eg.: personal.information@example.com.
3. Switch to anonymous user, that can visit the default JSON API path + user/user endpoint ( /jsonapi/user/user)
3. In the response payload the anonymous user or possibly any other user without "View user information" permission can see the usernames.

(and not just user/user endpoint exposing this information, any other API endpoint that contains a reference to a user, e.g.: author on nodes.)

Proposed resolution

Since we cannot just get rid of an old design decision without consequences, we must slowly phase out the old design. My proposed solution is introducing new permission called "view usernames" for controlling who can access usernames on a site, and this permission would not be granted to any default role/persona. This would be a similar change for New permission to view user email.

All existing system components need to be prepared that the username is not always available to everyone, e.g.: authoring information on nodes.

With the help of the entity access API, downstream developers can further extend the default access provided by core - which by default, would be restrictive instead of permissive about usernames - and for example allow visitors to view usernames of internal personas, like content editors, or would only grant access to user A's username to other (non-admin) users if user A explicitly granted access somehow (e.g.: by checking a checkbox...)

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

Issue fork drupal-3240913

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mxr576 created an issue. See original summary.

mxr576’s picture

Issue summary: View changes
mxr576’s picture

MR1289 showcases that solving the problem is doable, currently, only a few tests are failing which I would need some help with because I am having hard times understanding why JSON API tests are failing - my only idea is that caching is not properly tested on HEAD and GET requests.

cilefen’s picture

Regarding the Disclosure of usernames and user IDs is not considered a weakness policy, it would be worth discussing changing it in a new "[policy] ..."-titled issue and re-title this one to the task at hand, because this one as titled looks like a policy discussion at a glance.

mxr576’s picture

Title: Information stored in usernames is personal data » Make username access configurable and not implicit allowed
mxr576’s picture

Issue tags: -security hardening +Security, +Security improvements, +Privacy improvements
cilefen’s picture

@mxr576 Regarding comment #5 above, are you going to open a policy change issue to discuss?

If Drupal took the opposite stance on user enumeration, we would work through those issues in public (like this issue), and in some release consider it “done”, then thereafter any reports would be security issues that would get Security Advisories. If we don't change the policy, then future unexpected user enumeration bugs would be public.

mxr576’s picture

@cilefen, done, thanks for the suggestion

mxr576’s picture

Issue summary: View changes

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

mxr576’s picture

Version: 9.4.x-dev » 10.0.x-dev

Let's bump the version, based on the current activity on this issue I am a bit skeptical if this can land in 9.4.x. Also, considering the size of the suggested change, it fits better to a new major version.

Version: 10.0.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

bserem changed the visibility of the branch 3240913-information-stored-in to hidden.

bserem’s picture

Status: Active » Needs review

I'm taking a shot at this. I hid the old MR because it was 4 years old and I didn't feel like updating it.

Made a new MR (https://git.drupalcode.org/project/drupal/-/merge_requests/13617):

In my manual tests this works nicely for JSON:API, hiding usernames unless the new permission is granted.

Note:
Permission to View user information to anons allows them access to example.site/user/2 which could easily expose the username in the theme. This is not addressed in the MR and is not necessarily a core security issue.
Not much I can do about it, themers must check for permissions before rendering the usernames in that case. The work in the MR does help in that direction but doesn't solve that case, it can't.

Please have a look

vensires’s picture

Status: Needs review » Needs work

Just some thoughts in order to make sure we have taken everything API-related into account:

  1. The entity API operation to check for labels is "view label". This was previously returning AccessResut::allowed() and using MR!13617 we add our own logic in there.
  2. For view label to not fallback to view the entity class handler must set viewLabelOperation to TRUE; something already done by the core user module.
  3. The view label permission is used at least by JSON:API module, the EntityAutocomplete element API and the entity_reference_label field formatter.

Though I agree with the rest of the code introduced into the MR, I don't really like the following code:

// Allow for the special anonymous user.
// Normally this user profile can't be seen, but it is exposed in JSON:API
// by default.
if ($entity->isAnonymous()) {
  return AccessResult::allowed();
}

The main issue of this module is that we don't have the username access as a configurable option. By transferring this issue to the anonymous users only, I think we are actually hiding the issue in a subset of affected roles. I would prefer to not have this code and instead only check that the current role has the required permissions.
(EDIT: As @bserem explained to me in Slack, this check is there to make sure that User #0 has the "Anonymous" username listed, so my point above is invalid)

As for the rest of the code:

// Require both 'access user profiles' AND 'view usernames' permissions.
$access_result = AccessResult::allowedIfHasPermission($account, 'access user profiles')
  ->andIf(AccessResult::allowedIfHasPermission($account, 'view usernames'));

Can you explain why we need an AND? Wouldn't OR be more suitable? That way we could even just check if ($account->id() == $entity->id()) {...} and immediately after check for either of the following permissions: administer users, access user profiles or view usernames.

As a last step, just to make sure that nothing breaks in current installations - that's the reason I turn this into Needs work - we could make sure to have a post_update hook to assign the new view usernames permission to all roles.
Besides, using only permission-based checks we can make sure that anyone willing to change the system using the new Access Policy API is able to do so.

What do you think?

bserem’s picture

Assigned: Unassigned » bserem

Thanks for the feedback @vensires! Much appreciated.

I'm assigning the issue to me for now, but it might be a couple of days before I can jump to it again.
If anyone is willing to get this further by all means, be my guest.

@vensires from a quick overview I believe you got valid points, let me check them in detail and I'll get back to you with improvements.

bserem’s picture

Assigned: bserem » Unassigned
Status: Needs work » Needs review

Improved logic and order of checks, for better code readability. Also fixed CS issue added previously and adapted previous changes for new user registrations.

@vensires the idea behing AND is to do things the same as with emails:
- right now you are not allowed to view the email unless you have BOTH `view user email` AND `access user profiles`

I do not object to the idea of an update hook to give `view user names` to all people, however since this is actually an open security-related issue where usernames are exposed in JSON:API I would prefer not to do so, not yet. Many of the sites that have JSON:API enabled might not know that usernames are exposed. So it would be better for us to apply the fix, rather than apply the fix and at same time undo it by giving anonymous users the permission to see usernames.

Open to discussion of course, as always.

I'll bump this to needs review, mostly because I'd love a fresh set of eyes to check it before I continue working on it again.

Thanks

prudloff’s picture

Thanks for reviving this old issue, I agree this would be important to fix.

If you are using the email_registration module, jsonapi leaks the e-mail address of every user.
And even without email_registration, we noticed a lot of users tend to use their email address or their first name + last name as their username.
This is a GDPR violation and we are getting complaints from clients.

dimitriskr made their first commit to this issue’s fork.

smustgrave’s picture

Status: Needs review » Needs work

Have not fully reviewed but appears to have pipeline issues.

mxr576’s picture

I just realized I never came back to share the good news after we open sourced the modules we built to mitigate this problem. As a heads-up, solving it turned out to be a bit more complicated than I expected. Once usernames were placed behind strict access control, we also had to introduce extra steps for granting access, and there are still some open issues related to that, see issue queues.

https://www.drupal.org/project/view_usernames

https://www.drupal.org/project/view_usernames_node_author

vensires’s picture

@mxr576 thank you very much for coming back here and sharing your work on this as a contrib module!!!
It sure is a great place to start or to further decide what we expect core to have as features so that we can see this issue as "fixed".

Your module's code takes into account fields, JSON:API, bubble-ups and even emails.
Maybe we could focus on JSON:API and fields for this core issue?

bserem’s picture

I made an attempt and fixed the failing PHPUnit test. There are 2 more now but they seem related to gitlab artifacts and not to the changes in code.
I'm trying to understand what this is about.

vensires’s picture

@bserem thanks for the update on the last day of the year :)
I would also like to know your opinion on @mxr576's work and which of his achieved goals we would like to have in core too.

PS: Happy new year everyone! Best wishes to all of you!

bserem’s picture

I'm failing to fix this properly, mainly because many tests need to be updated for the new permissions, but that generates a very important question:

Do we update the tests to assume the new permissions are in place or do we update the tests to assume they don't have access to usernames?

I'm gonna stop here and wait for some feedback.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

prudloff’s picture

@bserem in order to keep the scope of this issue somewhat manageable, I think we should keep the existing behavior (so I guess grant the permission to everyone at install and in a postupdate hook?).
Being able to remove the permission will already be an improvement for site admins.
And then we can open a followup to discuss changing the default.