The user.module function user_roles contains a query to fetch roles with a certain permission from the permissions table, joined with the roles table. The permission is searched for using a LIKE query which is not the safest for a comma-separated list.
$result = db_query("SELECT r.* FROM {role} r INNER JOIN {permission} p ON r.rid = p.rid WHERE p.perm LIKE '%%%s%%' ORDER BY r.name", $permission);
The problem is that when you call the function like this (taken from the upload.module admin_settings):
$roles = user_roles(0, 'upload files');
You could also get the roles returned with permissions like 'dont upload files', 'upload filesystem'
I would suggest to change the way the permissions are stored in the permission table (comma separated, but without a space after the comma) and to change the query to:
$result = db_query("SELECT r.* FROM {role} r INNER JOIN {permission} p ON r.rid = p.rid WHERE FIND_IN_SET('%s',p.perm) ORDER BY r.name", $permission);
Comments
Comment #1
mcarbone commentedThis is definitely a bug, and I almost relied it on it while writing a patch for another issue (http://drupal.org/node/195161).
Also, this bug affects Drupal 6.
Comment #2
catchBumping to critical, although it's theoretical afaik, there's all kinds of potential gotchas here.
Comment #3
catchComment #4
hswong3i commented-1 for
FIND_IN_SET: it seems to be a MySQL specific SQL function. I am not able to search its document for PostgreSQL and Oracle.Comment #5
mcarbone commentedIs there a good solution for this that doesn't involve refactoring the permission table? I can think of several involving using REGEXP, or other non-trivial MySQL functions, but I imagine they would be considered bad practice. Other solutions would involve adding delimiters to the beginning and end of the string, so that LIKE can be used, but that doesn't seem ideal.
Comment #6
chx commentedNothing is broken here. This is a gotcha not a bug. We were fine with this, it'd be great if fixed but if went five or i dunno how many stable series like this, we can continue.
Comment #7
catchThis seems fair enough. There's no actual example of this breaking anything, if a contrib module did it, we could file an issue against that module.
Comment #8
mcarbone commentedHmm, I'm not so sure I agree with making this minor. Working on another patch that involved the 'post comments' and 'post comments without approval' (http://drupal.org/node/195161), I almost relied on this LIKE -- and then I realized that if this bug was ever fixed, my patch would break. In other words, one bug can lead to another.
In any case, I agree that it's not critical. Certainly D6 can launch with this if D5 has had it for a year. But I don't think that just because a function hasn't worked as advertised for a year that we might as well let it continue to not work. I think the priority should be 'normal,' but I won't bother switching it unless others agree.
Comment #9
teezee commentedFIND_IN_SET is MySQL specific.
Does anyone know why currently permissions are stored with comma's followed by a space? Is this done deliberately to be able to search for 'role name,' (note the comma). This is not the nicest way to work but neither is the LIKE part.
The user_admin_perm function saves the permission names. The following comment can be found there:
// Compile role array:
// Add a comma at the end so when searching for a permission, we can
// always search for "$perm," to make sure we do not confuse
// permissions that are substrings of each other.
I think the simplest thing to do here is to change the original query to:
Comment #10
mcarbone commentedteezee, what about this case?
"move files, remove files,"
Doing a LIKE '%move files,%' will still satisfy both. It can work that way only if a comma is put at the beginning of the permission string as well.
Comment #11
gábor hojtsyIMHO not minor. What we do at other places where a LIKE query is required on comma separated data sets, is rather ugly. We need to look for both prefixes and suffixes, ie. the following cases:
- only permission in permission list: column = '%s'
- permission in the beginning: column LIKE '%s,%%'
- permission in the middle: column LIKE '%%,%s,%%'
- permission at the end: column LIKE %%,%s'
So you OR all these together and get a long conditional. Don't tell me it is ugly, I know.
You can "help" this with
CONTACT(',',column,',') LIKE '%%,%s,%%'instead, so you add the commas in place for "easier" matching. BUT this introduces an expression instead of the column name, so each column will need its value passed through an expression. This generally looks good, because the field cannot be indexed, but this column cannot be indexed anyway...In this case, we have 2-10 rows I guess. I don't think people have more then 10 roles, but they might have a few more. Whether four conditionals OR-ed, or an on the fly expression performs better here can be measured.
Another option is to store the column with the leading and trailing comma, which makes the LIKE nicely controllable, ie it becomes
column LIKE '%%,%s,%%'. Then the loading logic needs to strip this off whenever a PHP array is created. This is a slight API modification, but gives us best performance. Would also need an update function.Comment #12
JirkaRybka commentedA nitpick: We have one space after each comma, so it needs to be either
column LIKE '%%, %s,%%'with comma+space added to start of stored string, or the storage format changed to remove that space.Comment #13
teezee commented@mcarbone, abolutely correct, is missed that one!
@Gábor Hojtsy, I agree that using a comma-prefix is (under current circumstances) the best way for now
In my view, the way these strings are stored is not the right one. I would like to suggest the following:
Saving the permission names is done in user_admin_perm_submit(), the query is built up like this:
A few things to change:
user.module / user_admin_perm()
user_admin_perm_submit()
Change the query creation in user_admin_perm_submit() to:
In this way, if any perms exist, they are stored like so: ',perm1,perm2,perm3,'
Change the LIKE part of the queries in user_roles() and user_filter()
Comment #14
gábor hojtsyteezee: at first look, these changes look fine. A few comments:
- user_admin_perm() also has a sizable comment on why the comma was there, so remove that too
- an update function is definitely needed to add these commas to existing roles in an upgrade
- a bit of a lookaround for more places where this change might need to be done
Comment #15
Taran commentedJust curious to hear from those closer to the code...
Do you think this bug could have caused this problem? :
http://drupal.org/node/201843
Comment #16
teezee commented@Taran #15
Nope, that was just a missing Anonymous user (uid 0 in users table must be available).
This issue is about the incorrect way permissions are stored
Comment #17
teezee commentedI found another tricky situation in user_access(). There, the same check is performed using the comma.
Use case:
There is a test.module which implements permissions 'move files' and 'remove files'. This is just a test case but what if you want to give a user permissions to remove files, but you do NOT want them to move files. The user_access function will return true in both cases!
I changed the version to Drupal 5.6 because this problem occurs (as far as i've been able to check) all through version 5.
Comment #18
mcarbone commentedThis affects 7.x-dev as well.
(teezee: The version should be set to the most recent version it affects, not the earliest, and then fixes can be backported as needed.)
Comment #19
mcarbone commentedScratch that. It has been fixed in D7: #73874: Normalize permissions table
I'll set to Drupal 6.x-dev, but with the fix in D7 being fairly non-trivial, maybe this should just become a won't fix.
Comment #20
manikan commentedChange the following line in user_admin_perm function in user/user.admin.inc
From:
if (strpos($role_permissions[$rid], $perm .',') !== FALSE) {
To:
if (strpos( ", ". $role_permissions[$rid], ", ". $perm .',') !== FALSE) {