views:

153

answers:

5

This is a recursive function I wrote to determine whether or not a given user is authorized to view content on a page. It is called in essentially the following fashion:

if(authorize($_SESSION['user']['user_id'], $necessaryClearance)){
    //Output restricted content
} else{
    //Inform user they are not authorized
}

Every user has a clearance level, as well as a clearance status. This allows an authorize function to be called with $clearance as a clearance level the user has to match or beat, a clearance status that a user has to match, or an array of statuses - any one of which the user can match. Generally, the $user_id is pulled from session data ($_SESSION['user']['$user_id'], which is refreshed from a database each page load), and the clearance is set explicitly either on a per-page or per-module basis.

//This function checks if the user is authorized to view the page
//It returns 1 if access is granted and a 0 if access is denied
function authorize($id, $clearance){
 //$clearance == array
    if (is_array($clearance)){
   //if yes Iterate array through Authorize($id, $clearance[])
  foreach($clearance as $userStatus){
   $tally += authorize ($id, $userStatus);

  }
   return $tally;
 //if no check if $clearenance is equal to a string
 }else if (is_string ($clearance)){
  $string = "SELECT status

  FROM users
      WHERE id = '$id'
      LIMIT 1";
  //If result returned.
  if($userData = mysql_fetch_array(Query($string))){
   if($clearance == $userData['status']){
    return 1;
   }else{ 
    return 0;
   }
  } else{
   return 0;
  }
  // if no check if $clearance is equal to a number  
 }else if(is_numeric($clearance)){
  $string = "SELECT level
      FROM users
      WHERE id = '$id'
      LIMIT 1";
  //If result returned
  if($userData = mysql_fetch_array(Query($string))){ 
   // if number is less than or equal to clearance level allow access
   if($userData['level'] <= $clearance){
    return 1;
   }else{ 
    return 0;
   }
  } else{
   return 0;
  }

 }else{
  //if nothing matches the page dies
  die('Authorization has failed.');
 }
}

Are there any glaring security flaws in the code?

+6  A: 

Yes. You're not doing any escaping on the $id parameter!

This means that your queries are susceptible to a SQL Injection attack.

Jacob Relkin
How is someone going to manage an SQL injection if they don't have a means of altering $id?
DeathMagus
Still bad practice to inject variables like that. Use parameter binding.
The Pixel Developer
Why is that? Obviously parameter binding prevents SQL injection, but I don't see how someone could alter $id to begin with.
DeathMagus
You don't show how the calling code defines $user_id, so one must assume it *could* come from an untrusted source (even if it is stored in your database), which means it could be spoofed.
Bill Karwin
I stated that $user_id (in all but hypothetical cases) is $_SESSION['user']['$user_id'] (which is updated from the database during each page reload.
DeathMagus
A: 

You have not provided enough information. Where is $id coming from? Is it a get/post/cookie value? Because if it is then you can just say $id=1. This is called "Insecure Direct Object Reference."

There is also the case SQL Injection. You could inject a simple tautology such as ' or 1=1 or do something more insidious such as ' and 0=1 union select "<?php eval($_GET[e])?>" into outfile /var/www/backdoor.php.

Rook
read the code and the coment! 1. "authorize($user_id, $necessaryClearance)" and 2. "...$user_id is pulled from session data..." only thing unclear is what kind of session data and where clearance is from and if clearance is mutable
ITroubs
It's pulled from $_SESSION['user']['$user_id'], which is refreshed from a database each page load.
DeathMagus
so my guess was right.
ITroubs
Yes but how did $user_id get into the database to begin with? It could have come from an untrusted source. Just because it's in your database doesn't mean it's safe.
Bill Karwin
Would that not be a separate security concern? If I start out by assuming my database is wrong, then I can't really use it at all for security purposes.
DeathMagus
Now you're getting it!
Bill Karwin
@DeathMagus why are you resetting the $_SESSION variable on each page load? $_SESSION exists because it remains static for the entire session.
Rook
@Bill Karwin If I can't use my database *at all* for security, then I have no means of checking anything at all. I assume you mean something different than "Now you're getting it" in response to my comment?
DeathMagus
@Rook I reset the $_SESSION['user'] variable on each page load so that any changes made to that user's information will propagate immediately. If an admin changes a person's permissions level from 3 to 2, for instance, then that person should have a permissions level of 2 as soon as they load their next page.
DeathMagus
@DeathMagus: I don't mean you can't use the database *at all*. I mean you shouldn't rely on data being immune to hacking. Trust, but verify. Use multiple layers of protection. Like coercing $user_id to an integer as I showed in my answer.
Bill Karwin
A: 

As long as you save the user_id in a variable that in no way is changable by the user (i.e. $_SESSION) and $clearance is not mutable, too, then you should be safe.

ITroubs
This is my perception, as well, but clearly many disagree. Any idea why they disagree?
DeathMagus
Well i made an "educated guess". They disagree to show that it might be possible in any other code part which is influencing the $user_id var and the $necessaryClearance.
ITroubs
+2  A: 

SQL Injection is a serious risk and you should do whatever you can to defend against it. Even if you think your $user_id comes from session data, you still have to consider the source of the session data. You say it's the database, but how did it get into the database?

Just code defensively. It's very simply and easy to do in this case -- just coerce $user_id to an integer and you can be sure no extra SQL syntax will come along for the ride as you interpolate it into your query.

Also, it's unnecessary to use recursion for your function. Here's an example of doing the same function in a more simple manner:

function authorize($user_id, $clearance) {
  // coerce to integer to defend against SQL Injection
  $user_id = (int) $user_id;

  $sql = "SELECT status FROM users WHERE id = {$user_id}";
  $userData = mysql_fetch_array(Query($sql));

  $tally = 0;
  foreach ((array) $clearance as $userStatus) {
    if (is_numeric($userStatus)) {
      $tally += ($userData["level"] <= $userStatus);
    } else {
      $tally += ($userData["status"] == $userStatus);
    }
  }

  return $tally;
}

The only thing this simpler code doesn't support is nested arrays in $clearance. But do you really need to support that?

PS: I also recommend you switch to PDO. It's easy to use and supports SQL queries with parameters, which is an even better defense against SQL injection. For example:

    $sql = "SELECT status FROM users WHERE id = ?";
    $stmt = $pdo->prepare($sql);
    $result = $stmt->execute(array($user_id));
    $userData = $stmt->fetch();
Bill Karwin
Thanks for the explanation. I'll definitely look into PDO. Simply coercing $user_id into an integer doesn't seem to help, however, since if someone can change that value they would only need to send a value that had administrator privileges, rather than injecting an entirely separate SQL query. Also, since the database field that $_SESSION['user']['user_id] is populated from restricts entries to integer values anyway, isn't the typecasting superfluous?
DeathMagus
Good point, if you're sure the source of the data is an integer column in a database, *and* you can do a code inspection to trace the whole lifecycle of that session data, confirming that it can't come from any other source, then you may have enough assurance of safety. I would still leave the int coercion in, though. It's a cheap operation, and I prefer redundant security.
Bill Karwin
In your example, the SQL query should be above the foreach, since it will return the same result with each iteration. Also, PDO::prepare() is more intended for a statement that will be executed multiple times with different parameters. This allows the database server to optimize the query once rather than each time it is run. I'm not sure of the impact of doing it this way, but I prefer something more like `$sql = 'SELECT status FROM users WHERE id = '.$pdo->quote($user_id); $result = $pdo->query($sql); $userData = $result->fetch();` (can I do multi-line code in a comment?)
JamesArmes
@JamesArmes According to PHP.net, PDF::prepare() is the preferred method, partly because it allows the SQL server to cache the general query form. As an authorization function would be used frequently, I imagine that could be quite beneficial.
DeathMagus
@DeathMagus This is only true if the query is to be run multiple times from the same statement. If the same query is going to be run multiple times for the same user it would benefit from the query cache, which is not used for prepared statements in MySQL prior to 5.1.17.
JamesArmes
@JamesArmes: Yes, I agree. I'll edit the code with your suggestion.
Bill Karwin
A: 

Even if you know 100% where $id is coming from right now, it's not safe to assume that that will always be the case. What if your application grows? What if you have more people working on it? What if they call this function with different values beside the $_SESSION values? Sure you might know the ins and outs of your app and you know that that might never happen but it's still bad practice. At the very least, you can use mysql_real_escape_string.

EDIT It's better to secure things at the last point of entry. Otherwise you leave open doors. If our job was to make sure that absolutely no passengers with bombs entered a plane, where would be the safest, most secure place to verify that? In the parking lot outside, at the airport's front door, or right before the passenger boards the plane?

shideon