Messing with the global user in Drupal 7

Messing with the global user in Drupal 7

There are times when you need to run some code as a specific user. You might want to send an email and the content is subject to the receiving users’ permissions on the site. Sometimes you might need to run some code as user 1 and not the logged-in user.

In situations like these, you need to change the global user to the user you want to impersonate.

It’s not that difficult to do so. Here is an example.

global $user;  
$user = user_load(1);

// your code here.

This example is soooo wrong. Ok, you change the global user object to user 1 so now the logged-in user has super admin access to your site…

A better example is the following

global $user;  
$old\_user = clone $user;  
$user = user_load(1);

// your code here.

$user = $old_user;

Ok, that’s better. We change the user to user 1, we do what we have to do, and then we reassign the old user to the global user object. Everything is back to where it was supposed to be and we are all happy.

But we are humans and we make mistakes. And if these mistakes are made in our code we might end up with exceptions thrown. So let’s take a more careful look at the example above. We have cloned the logged-in user object and kept it in a variable. Then we assigned user 1 to the global user. Then we run my code and finally, we reassigned the cloned user object to the global one. Oh, wait… what if our code running as user 1 throws an exception. The global user will not be reassigned the right value and we are stuck with a user logged in as user 1! THAT IS BAD! If the user realizes what’s happening they might just hack the hell out of our site with our permission.

So let's refactor the example a bit to make it “immune” to exceptions.

global $user;  
$old\_user = clone $user;

try {  
  $user = user\_load(1);  
  // your code here.

}

finally {  
  $user = $old\_user;  
}

Phew… now it looks ok. We wrapped our code in a try-finally block so even if our code throws an exception it will return the global user to the right one in the ‘finally’ part.

But there is another vulnerability here. Did you find it?

Me neither. A user of the site may find it though.

It doesn’t have to do exactly with impersonating a user. In some parts of your code, you may need to print information for a specific user. For example, we had a blog and we want to print the name of a post author with a link to a page with this author’s posts. So we go about it like this…

$user = user_load(<user_id>);  
print $user->name;

Cool, we printed the name of our user on the blog post page. But what if within the same scope someone (or maybe us) had used the global user the same way we used in the examples above

global $user;

We wanted to just print a user’s name but we also changed the global user to the user who authored the blog post. What if user 1 had authored this blog post. Now anyone reading this post would have ‘root’ access to our site. THIS IS BAD!

The solution to this problem for me is to use $GLOBALS['user'] instead of global $user; whenever we want to use the global user in any way.

If we want to make it even more robust we should disable saving session data whenever we change the global user like so

$old_user = clone $GLOBALS['user'];  
$old_session = drupal_save_session();

try {  
  drupal_save_session(FALSE);  
  $GLOBALS['user'] = user_load(1);  
  // our code  
}  
finally {  
  $GLOBALS['user'] = $old_user;  
  drupal_save_session($old_session);  
}

Nice, now we can be sure that the global user will not be affected by whatever we do in there.

But it’s too much code to write every time we need to impersonate a user. Let's make it a function.

<?php
function run_as_user($tmp_user, $function) {
  if (!is_callable($function)) {
    return FALSE;
  }
  $args = func_get_args();
  array_shift($args); // remove $tmp_user
  array_shift($args); // remove $function 

  $old_user = clone $GLOBALS['user'];
  $old_session = drupal_save_session();
  try {
    drupal_save_session(FALSE);
    $GLOBALS['user'] = $tmp_user;
    return call_user_func_array($function, $args);
  }
  finally {
    $GLOBALS['user'] = $old_user;
    drupal_save_session($old_session);
  }
  return FALSE;
}

function run_as_user_by_id($uid, $function) {
  if ($tmp_user = user_load($uid)) {
    $args = func_get_args();
    $args[0] = $tmp_user;
    return call_user_func_array('run_as_user', $args);
  }

  return FALSE;
}

function run_as_user_by_mail($email, $function) {
  if ($tmp_user = user_load_by_mail($email)) {
    $args = func_get_args();
    $args[0] = $tmp_user;
    return call_user_func_array('run_as_user', $args);
  }

  return FALSE;
}

function run_as_user_by_name($name, $function) {
  if ($tmp_user = user_load_by_name($name)) {
    $args = func_get_args();
    $args[0] = $tmp_user;
    return call_user_func_array('run_as_user', $args);
  }

  return FALSE;
}

// EXAMPLE
run_as_user_by_id(1, function() {
  // code to execute as user 1
});

Some things are happening here.

First, we create the function run_as_user which takes a user object and a callable as arguments. Any extra arguments will be passed to the callable as its arguments (using call_user_func_array).

Then we create three utility functions so that we can impersonate a user by id, email, and name.

At the end, we can see a usage example.

Conclusion

Messing with the global user might be very dangerous on a drupal site. It could make an anonymous user super administrator. My suggestion is not to use global $user syntax as someone might use the user variable for other stuff. Instead, we should use the $GLOBALS['user'] to refer to the global user object. I also gave a code snippet I use for safe user impersonation within my projects.

Do you think my code is not safe enough, or just stupid? Write a comment to put me in my place.

Did you find this article valuable?

Support Stavros Ioannidis by becoming a sponsor. Any amount is appreciated!