Problem/Motivation
This is approved as a public security hardening in #25.
Access to modify a users role is handled inconsistently. The user access control handler incorrectly allows users with administer users to modify roles. The account form correctly requires administer permissions to edit roles of a user since it is implementing its own form elements.
The permission administer permissions is meant to be used to assign roles to users, CRUD roles, and to edit permissions grants for each role. Not administer users.
Drupal 7
In Drupal 7, when you go to a users page, you can not edit the roles of the user. Nor can you use the bulk "[Assign/Remove] a role from the selected users" operation on admin/people. Unless you have administer permissions.
Drupal 8
In Drupal 8, if you do not have administer permissions, you also cannot edit the roles when you are on the /user/%/edit page.
However if you have administer users, you can go to /admin/people and use the 'add the XXXX role to the selected users' actions.
This is a problem, because the user with administer users can assign himself the built-in 'Administrator' role. Which gives him all permissions, including the ability to edit permissions.
This effectively makes administer permissions useless.
Proposed resolution
- Disallow users without
administer permissions
to edit the user roles in the user edit form. - Add a mechanism to determine if the current user is allowed to use a specific action. Note that the actual action plugin
::access()
method is only a run-time check, checking if the action can be done by the current user, on a specific object/entity. But we need more generic check to see if a user is able to read/access an action before the action starts to be executed. The method could be used, mainly, on views with bulk operation form, where the list of actions should be limited to the actions that the current user is allowed to access. - Use the above check to filter out the add/remove role operations from the bulk operations form on
/admin/people
view.
Remaining tasks
None.
User interface changes
Users without administer permissions
permission will not see anymore:
- The roles field on user account edit form.
- The add/remove role actions, in the bulk operations selector, on user administrative page, at
/admin/people
API changes
- The action entity defines now its own access control handler in the annotation, pointing to:
\Drupal\system\ActionAccessControlHandler
ActionAccessControlHandler::checkAccess()
will grant the access to the action, first, if the current user hasadminister actions
permission. If not, will ask the plugin to make a decision.- New method
::userAccess()
in the pluginActionInterface
interface. ActionBase::userAccess()
will returnTRUE
. In this way we ensure the Backward Compatibility- Implement
ChangeUserRoleBase::userAccess()
by checking the user permission. - In
\Drupal\views\Plugin\views\field\BulkForm
, limit the bulk operation action options based on action entity::access()
.
Data model changes
None.
Original report by kevin.dutra
Access to the roles
field on the User
entity is not handled consistently. This gap has been made possible due to the sort of non-standard approach that the AccountForm
takes by providing form elements for fields directly in the form builder instead of using the field system to handle form display with normal widgets. Since it's providing the form elements, it's also setting the #access
property, but not in a manner that is consistent with the actual field access. As a result, there is a disconnect between what can be done via the UI and via the REST API for the same user.
The AccountForm
only allows access to the roles
field for a user with administer permissions
.
$form['account']['roles'] = array(
'#type' => 'checkboxes',
'#title' => $this->t('Roles'),
'#default_value' => (!$register ? $account->getRoles() : array()),
'#options' => $roles,
'#access' => $roles && $user->hasPermission('administer permissions'),
);
However, the UserAccessControlHandler
allows anyone with administer users
access to the roles
field.
// Administrative users are allowed to edit and view all fields.
if (!in_array($field_definition->getName(), $explicit_check_fields) && $account->hasPermission('administer users')) {
return AccessResult::allowed()->cachePerPermissions();
}
Left to be done
- Decide on who should actually have access to the field
- Create a patch
Comment | File | Size | Author |
---|---|---|---|
#90 | 2846365-90.patch | 20.95 KB | Suresh Prabhu Parkala |
#89 | diff_83-88.txt | 18.25 KB | Marios Anagnostopoulos |
#89 | 2846365-88.patch | 21.49 KB | Marios Anagnostopoulos |
#83 | interdiff_82-83.txt | 4.73 KB | ranjith_kumar_k_u |
#83 | 2846365-83.patch | 21.2 KB | ranjith_kumar_k_u |
Issue fork drupal-2846365
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
Comment #2
kevin.dutra CreditAttribution: kevin.dutra at Workday, Inc. commentedI'm happy to provide a patch, but I can't really make the call on which way the access should really work.
On the one hand,
administer users
is the admin permission for theUser
entity, so it seems like it should have access to all the fields. But on the other hand, I can understand the desire to restrict this specific field because it governs what that user will have access to, so you may not want someone to be able to grant every role, even though they can edit all the other fields. (You may want to use something like the Role Delegation module so that they can only choose from an allowed set of roles.)Comment #3
dpiAnything to do with roles should be controlled by administer permissions. Including CRUDL role entities, editing permissions for roles, or assigning roles to users. The account form isnt the only place that uses the correct permission to edit roles for users.
Seems like a oversight/bug.
Comment #4
dpiUploading to see what kind of things this breaks.
Comment #5
dpiNot that one, this one.
Comment #8
dpiStill working on it.
Comment #9
dpiSecurity issue Users with 'administer users' permission can gain full access relates to this issue.
The vulnerability was approved to be fixed publicly since it requires a role with the somewhat trusted administer users permission.
Comment #10
dpiComment #11
dpiMoved majority of security submission to issue summary, original issue summary left intact
Comment #12
dpiTests only
Comment #14
dpiComment #10 contains the patch for review.
Comment #15
kevin.dutra CreditAttribution: kevin.dutra at Workday, Inc. commentedEverything looks good to me, but the one thing I'm wondering is: should we also update the form element on the account form so that it's actually invoking the access control handler? From a DX perspective, if I wanted to tweak the way the access worked for that field, I'd implement the entity field access hook, but then I'd also have to go and alter the form currently, which is pretty dorky.
Comment #16
jibranNW for comment fix other than that this RTBC.
and most fields? Let's rewrite this compelety.
This is the only fix and using admin field totally makes sense here.
Comment #17
dpiI think this makes sense. Attached a change which uses the same method the role action plugins use to determine access.
Agreed and done.
Comment #18
jibranLet's see if testbot agrees.
Comment #19
kevin.dutra CreditAttribution: kevin.dutra at Workday, Inc. commentedNicely done. Thanks for the patch @dpi! :)
Comment #22
dpiRandom CI failure
Comment #23
catchI'm going to move it to 8.4.x, while it's security hardening, it's potentially disruptive to sites relying on the existing behaviour.
Comment #24
moshe weitzman CreditAttribution: moshe weitzman commentedI reviewed this patch for code and test coverage and it looks solid. Thanks for untangling these permissions ... 8.4 sounds reasonable to me as well.
Comment #25
xjmYeah, it's clear to me that the permission is wrongly implemented from the summary, but "fixing" it if they are already relying on D8 behavior can lock people out of their sites. So we need to be careful about fixing it.
Also, just wanted to state explicitly that this is okay as a public issue, because the permission is already marked as a restricted admin permission.
I wonder if a PSA would be appropriate to inform people of the current behavior and the upcoming change? Or is that too big a deal for what is already an admin permission? Other than that, at least I think we will need a change record and release notes mention for 8.4.0.
Comment #26
xjmCrossposted. Thanks @moshe weitzman!
Comment #27
xjmComment #28
xjmComment #29
xjmFirst one. :)
Comment #30
xjmNW for the CR, at least.
Comment #31
dpiCreated a change record: Required permissions to use add or remove role actions changed
Comment #32
dpiComment #33
xjmThanks @dpi for the change record draft! I updated its title to be more clear, since this is an important change users need to understand right away: Administer users permission no longer allows assigning roles.
I think the change record could also use some additional improvements for clarity. "Traditionally" doesn't make sense here. We can just say "In Drupal 7" since this is a change record. It also does not make it clear in the text what exactly was changed in the text. Also, adding headers so the user can quickly update the "before" and "after" would be helpful. What does look really good is the very clear explanation of the impact of this change.
I'll try to propose some improvements.
Comment #34
xjmAlright, I did an extensive update of the change record: https://www.drupal.org/node/2853612. I removed the screenshot because it was confusing as it focused on the past loophole, when actually the CR should communicate the current state to users. I also rewrote it focusing on the before-and-after and what the site owner would need to know. We can then reference the CR in the release notes.
Comment #35
xjm(Updating issue credit.)
Comment #36
dpiCR changes look great, thanks!
Comment #37
xjmLooks like this never got set back to RTBC for the change record review, so doing that now. Thanks @dpi.
Comment #39
xjmIt was the first one, but it's not one anymore. :P
Comment #40
xjmSo there are a couple of small things that need work in this patch (I did not do a full review in #37):
Since we now have two accounts, one that has admin users but not admin roles, and one that has admin roles but not admin user, I think we should probably rename the
$admin
property and theadmin
user name to beadmin_users
instead. Otherwise, the test is difficult to read and confusing.This docblock is incorrect.
The variable name also is a copy-and-paste name that does not fit this new access provider.
All of these old-style arrays should be converted to the
[]
syntax.These are all small cleanups that could be done by a novice contributor. Thanks!
Comment #41
Dinesh18 CreditAttribution: Dinesh18 as a volunteer commentedNeeds reroll
Comment #42
jofitz CreditAttribution: jofitz at ComputerMinds commentedStraight re-roll (before addressing @xjm's comments in #40).
Comment #43
jofitz CreditAttribution: jofitz at ComputerMinds commentedAddressed @xjm's comments in #40.
passwordAccessProvider()
).Comment #46
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedPlease could the Core experts consider another potential security aspect - at present there is a way in which this fix could reduce security.
Summary
AccountForm::form()
calls $account->roles->access().hook_entity_field_access
being invoked based on the current roles.Example
An example is the module Administer Users by Role (where I am a maintainer). It grants access to "sub-admin" users to
hook_entity_access
hook_entity_field_access
.In D8.4, this code is perfectly safe. The roles field is by default not present on /user/XX/edit.
When this fix is applied, this code has a security bug. The roles field is by default present on /user/XX/edit and all roles are available, including admin. The sub-admin can assign admin permission to themselves.
The fix is for the custom code to use hook_form_FORM_ID_alter to filter 'roles' to include only safe values. Fortunately "Administer Users by Role" already does this. However it seems possible that other code exists that doesn't.
Proposal
Ideally AccountForm::Form would automatically reduce the list of roles to only those allowed. However I can't see how.
Fallback is to ensure that when the form verifies, the actual assigned roles are checked again with
$account->roles->access
.Comment #47
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedFor a module to customise access to roles (e.g. to assign specific roles but not other roles) it needs
hook_entity_field_access
for 'roles'hook _form_user_form_alter
for 'roles' to set #access and #optionsUnless someone can see a clever way to do both parts of the second bullet automatically then I reckon the safest approach is to leave AccountForm.php unchanged in this patch. Changing #access but not #options is not safe or helpful.
Comment #48
kevin.dutra CreditAttribution: kevin.dutra at Workday, Inc. commented@AdamPS, limiting access to assign individual roles is an entirely different issue and not one that would be addressed here. The functionality you're describing sounds a lot like the Role Delegation module. You might want to check out their approach.
Comment #49
AdamPS CreditAttribution: AdamPS at AlbanyWeb commented@kevin.dutra Thanks for the response.
The background to #47 is in #46. In summary, I believe the current patch is insecure. Now I may be wrong and I'm quite happy to have someone explain why (but this is not just theoretical - I did an actual test). However my comment is not "an entirely different issue" - I am talking about the security of this patch. Apologies if I didn't make that clear.
I'll try and explain the scenario more clearly.
This custom module is perfectly safe in D8.4, but not safe after the patch.
Comment #50
kevin.dutra CreditAttribution: kevin.dutra at Workday, Inc. commented@AdamPS, as far as I can tell, the behavior you are describing is the expected behavior. When you grant access to a field like the
roles
, then yes, you would have access to all of the roles that exist. This is why the out of the box core behavior is to only allow users with theadminister permissions
permission because this is a very sensitive field. (As you've described in the exploit where a user could augment their own access.)If a third party module extends access to that field to additional users, it would be that module's responsibility to do it in a secure manner. Even without this patch, if a third party module is extending access in the manner you describe, it is already allowing the exploit you've described if they can operate via the REST API, a bulk action in a View, etc.
This issue is all about bringing access control into alignment. And yes, this would make it more visible if there was an issue with existing implementations of field access on this field because the user form would finally be subject to the same access as gets used everywhere else. Does that explanation make sense?
Comment #51
AdamPS CreditAttribution: AdamPS at AlbanyWeb commented@kevin.dutra Thanks for a clear explanation. The point at which my understanding differs is
With my hook
access('update, 'roles'. [])
which succeeds.access('update, 'roles'. ['admin'])
which fails.So it's all safe. However including this patch, if the user (with no roles) accesses user/edit to add admin role, that leads to
access('update, 'roles'. [])
, which succeeds.The problem is as I said in #46: that the patch is calling
$account->roles->access()
passing the current value of role then allowing the user to assign an arbitrary new value. The correct usage ofaccess
is to pass the new value.The key to my argument is that access check is not just "can user U access field F" but rather "can user U access field F to set value V" - where V is the $items parameter.
Comment #52
kevin.dutra CreditAttribution: kevin.dutra at Workday, Inc. commentedAh, I see what you're getting at. The problem is, I don't think the API really guarantees when the access check is going to occur, so you would not be able to use the values of the field in that manner. It does provide a
FieldItemList
as an argument, but field access can be checked at any time, so it might be done on the current entity as-is or it might be done on they entity with changes. (I believe this is one of the pain points that Role Delegation ran into and why they changed their design to create a totally separate form for allowing users to modify roles that was not subject to the same access requirements.)Anywho, the change record is there to give notice of the change and allow contributed modules the ability to react to it. IMHO, although it does produce different results for that one use case, I don't think it should stop the patch from continuing as-is. It reduces the number of one-offs floating around, and if someone wanted to do something along the lines of the code sample from above, it wouldn't be much more effort to throw in a quick form alter to change the
#access
property of the role field.Comment #53
AdamPS CreditAttribution: AdamPS at AlbanyWeb commented@kevin.dutra I am rather baffled by your comment. It's great that, as far as I understand, you now accept my argument that this patch introduces a new security vulnerability. However I can't understand why you seem to think it's fine to go ahead anyway (it feels rather dismissive of my point of view:-))
Unless I have missed something, this issue would still be solved even without the change to AccountForm - so why can't we go that way? You suggest "It reduces the number of one-offs floating around" which is a reasonable but low-priority argument - surely way, way less important than security?
I am not just commenting here to make your life difficult! I am the maintainer of Administer Users by Role. The latest D8 version has I hope now successfully achieved the goal of allowing granular assignment of permission to edit users and assign roles fully integrated with Core - using Entity Access API, the existing forms, the existing Actions and so on. It wasn't altogether easy, and has lead to some tidy up of Core #2921112: [META] User access control that is progressing towards getting committed.
The access API has a field $items, which as far as I can see when called from REST API, Views Bulk etc. contains the new value. This gives D8 an access API where the decision can depend on the value. Surely this is a good thing - why try to throw it away? I struggle to see how the $items parameter can be used unless it has this meaning.
How would someone know they needed to do that? What if another contrib module had copied the the code from Core assuming it was good code? - there would be a missing form alter for the copied code. Why make it more effort for Drupal to be secure as part of a "security hardening" patch:-)?
For the record, the change record does not seem to make any mention that this change makes Drupal less secure in certain scenarios - and quite rightly so. Please don't edit the CR to say that - let's change the patch instead.
Comment #54
kevin.dutra CreditAttribution: kevin.dutra at Workday, Inc. commentedHi @AdamPS, I think you're misinterpreting what I'm saying. In no way do I accept that this patch introduces a security vulnerability. Quite the opposite, actually. By allowing the user form to function differently than the access control set up everywhere else, it is inherently less secure, as a third party module can adjust the field access via a hook in an incorrect manner, but the form would give them the false impression that it was still secure.
I'm not at all suggesting that the API should be changed, but it would be good if it were better documented. It's perfectly fine to make use of the field items, but you have to understand that the contents therein may be current values or proposed new values, just depending on when the access is being checked. As the documentation suggests, it's not even guaranteed to be provided to you, as it's an optional parameter. (For example, when Views is deciding whether to render a column, it does an access check without any $items to see if the user has access to the field. If they don't, it omits the column altogether.)
There's nothing wrong with the code. I'm not sure what core code they would be copying from core that you think would be problematic. Suffice it to say that if someone is creating an access module and they don't have a fully understanding of the access API and the ramifications of the changes they are introducing, or have sufficient test coverage to catch discrepancies with their assumptions of how things should work, then they have no business releasing that code into the wild.
I understand that you're the maintainer of Administer Users by Role, and I appreciate your point of view. My aim is not to be antagonistic, but I disagree with the notion that the patch makes things less secure. It merely doesn't work the way you want it to with your current implementation. Like I said, I don't think the current documentation around the $items parameter is sufficient and I think that's the main pain point here. If that was better communicated, then I think people will better understand the risk/limitations of trying to make use of field values to check access. If we create a followup to improve that documentation, would you agree that the user form change becomes a non-issue?
Comment #55
AdamPS CreditAttribution: AdamPS at AlbanyWeb commentedThanks for the follow up.
Certainly we can agree on that.
Wait a mo. I thought I explained in detail a specific scenario in which this patch changes a site from being secure to not being secure, and in #52 you said "Ah, I see what you're getting at." Please can you clarify? Aha I guess that in your view the code in the specific scenario was wrong in the first place, because of the next point. Now "I see what you are getting at":-).
So this seems the key to it all. Please can you explain what evidence you have to support this? So far you have merely asserted that my interpretation is wrong and yours is right:-)
I realise you may have an overwhelming reason that I don't know about. But otherwise I don't favour that interpretation as it seems confusing and hard to use. I can't see how to control access to allow one value but not another. I can't see how to write secure code that references $items (other than to call ->getEntity() etc.). I can't see how to grant field access to 'roles' (except to a full unlimited administrator - who presumably has 'administer permissions' anyway so there is no point). Because in each case, the attacker can arrange for their old value to pass your access test, then supply an arbitrary new value.
It's a shame as it seems to prevent modules like "Administer Users by Role" from granting selective field access on 'roles' (and same for any other field where access needs to depend on value).
BTW I am already aware that $items an optional parameter, and I don't see any difficulty with that case - I'd say it's the non-NULL that we should focus on.
Not yet - but if you convince me of the previous point then yes.
Comment #56
AdamPS CreditAttribution: AdamPS at AlbanyWeb commented@kevin.dutra I have raised #2925042: Unclear documentation for field access - please can you read it and add your comments?
Comment #57
idebr CreditAttribution: idebr at iO commentedRelated / duplicate of #151311: Split 'administer permissions' into a new administer roles permissions
Comment #58
AdamPS CreditAttribution: AdamPS at AlbanyWeb commented@kevin.dutra With some help from @Berdir on #2925042: Unclear documentation for field access I have found the evidence that backs up your claim about how the access functions should be used. I no longer have any concerns from a security perspective and I withdraw my request from #46. Thanks for your patience, and I hope you will agree it's worth taking time to remove security doubts. In this case the debate has lead me to discover there was a security flaw in the alpha release of the Administer Users by Role module, so thank you.
I have one minor item of feedback on the patch
Why not just
$account->roles->access('edit')
?Comment #60
JacobSanford@Jo Fitzgerald's patch in #43 no longer applied to 8.6.x. A reroll with no further modifications is attached. @AdamPS's suggestion to simplify the access check has not been implemented.
Comment #61
zahord CreditAttribution: zahord as a volunteer and at Skilld commentedchanging status to allow tests last patch
Comment #63
claudiu.cristeaAlso, this, being a bug is eligible for 8.5.x.
Comment #64
AdamPS CreditAttribution: AdamPS at AlbanyWeb commented@claudiu.cristea Nice work, good to get this issue moving again. I think the review comment from #58 is still valid.
Why not just
$account->roles->access('edit')
?Comment #65
claudiu.cristeaHowever, we are facing a UX problem here. We are showing the option (eg "Add the XXXX role to the selected user(s)") to the unprivileged user but in reality he's not able to execute that operation, receiving an error message. What a flawed user experience for the 3rd millennium AD!!! Instead, additionally to what has been done so far, we should also hide the options. But this is hard to do because the action plugin access is checking the access on a per-object (entity) basis. So, it's a runtime check. We need a check that doesn't go that far, we have to check if the current user is able to access that action. For this reason, this patch is expanding a little bit the scope of this issue:
action
config entity defines now an access handler.ActionInterface::userAccess()
.ActionBase
abstract implements::userAccess()
by returningTRUE
. In this way, we ensure the BC.::userAccess()
also inChangeUserRoleBase
and we check there for theadminister permissions
permission.BulkForm
Views field plugin, we check also the access when we build the list of actions.The patch contains also the #58 fix.
Comment #67
claudiu.cristeaFixed some docs and test failures. If the user has 'administer actions', permission, that should prevail over plugin check.
Comment #68
claudiu.cristeaUpdated the IS to reflect the patch. Updated the CR and, yes, because this introduces kind of disruption, it should go in 8.6.x. Changed the title to reflect that this is a regression from D7.
Comment #69
idimopoulos CreditAttribution: idimopoulos at Randstad Digital for European Commission and European Union Institutions, Agencies and Bodies commentedI haven't done a full review on this, just some local tests. This line troubles me a bit though.
What this does is that it allows the user to view the action if he has the entity type's admin permission OR if the plugin allows it.
However, if the user has the 'administer actions' permission, and not the 'administer permissions' permission, in a bulk form, he will be able to view the Add/Remove roles actions but if he attempts to execute them, he will get an access denied error.
While this fixes the security issue of the ticket, it is a bad UX as - since there are not per-action permissions - we give the user the impression that he can mangle with the roles of the user until he actually tries to execute the action.
Initially I was thinking that an andIf would be a solution to that but in reality, this would mean that it would not suffice that the user would have the administer permission of the entity type.
For the record, I do not think that this makes the current implementation a bad approach, I just think that we still lack a per-action permissions system to take care of that.
Comment #72
trubach CreditAttribution: trubach commentedReroll of #67 that applies to 8.7.x and 8.8.x
Comment #78
Warped CreditAttribution: Warped as a volunteer commentedThis issue seems to have been lost in the shuffle for 2 years? I'm experiencing it on 8.9.13
The manager who has the right to create accounts and grant elevated roles to users is not allowed to change role permissions as they could change the way the site is configured. Changes that require more knowledge about how Drupal works than someone who is just managing users would not normally need to know. Changes that would have been based on decisions when the site was created. The fact that "Roles" are not displayed on the user maintenance screen, but are available on the bulk operations is inconsistent and may not be noticed because the bulk operations must be viewed as a drop down selection. Granted, the https://www.drupal.org/project/role_delegation and https://www.drupal.org/project/administerusersbyrole are options, but another user might be the one delegated to control permissions. Then an additional module would not be required.
Changes should be made one way or the other to avoid inconsistencies. Either allow both bulk operations AND the edit profile form to change the roles (by removing the authority check from the edit profile form), or remove the options from the bulk operations list (by applying the patch above). Or maybe a 3rd way of adding "Change user roles" and "Change own roles" to core. Then all that would be needed additionally, if wanted, would be an option to limit changing roles to those below your own role, or a selection of available roles allowed.
Comment #81
s.messaris CreditAttribution: s.messaris at Web Bunch commentedStill relevant in 9.3.
Patch doesn't apply.
Comment #82
Emberhood CreditAttribution: Emberhood at Web Bunch commentedReroll of #67 that applies to 9.3.x and 9.4.x
Comment #83
ranjith_kumar_k_u CreditAttribution: ranjith_kumar_k_u at Zyxware Technologies commentedComment #88
Marios Anagnostopoulos CreditAttribution: Marios Anagnostopoulos as a volunteer commentedHere is a reroll for 9.5.10 for anyone who might need it.
Leaving is as needs work until someone posts a patch for at least 10.x
Comment #89
Marios Anagnostopoulos CreditAttribution: Marios Anagnostopoulos as a volunteer commentedRe-uploading the same patch with the correct name.
Also adding an interdiff.
Comment #90
Suresh Prabhu Parkala CreditAttribution: Suresh Prabhu Parkala at Specbee commentedPatch for the latest 11.x. Please review.
Comment #91
smustgrave CreditAttribution: smustgrave at Mobomo commentedPlease include interdiffs with patches or a comment of what you changed.
The last patch doesn't pass commit checks, could you make sure to run
./core/scripts/dev/commit-code-check.sh
before uploading a patch to make sure there are no issues with code formatting. see https://www.drupal.org/docs/develop/development-tools/running-core-devel...