Yii 1.1: Configuring controller access rules to default-deny

11 followers

Starting with the blog tutorial, Yii developers are familiar with the notion of access rules defined in the controller, where the actions are allowed or denied depending on the user's name or role.

class CommentController extends CController {
    public function filters()
    {
        return array( 'accessControl' ); // perform access control for CRUD operations
    }
 
    public function accessRules()
    {
        return array(
            array('allow', // allow authenticated users to access all actions
                'users'=>array('@'),
            ),
            array('deny'),
        );
    }
    ...

Access rules — when enabled with the accessControl token in filters() — are processed in order, from top to bottom, stopping at the first match. It's a common practice to place a deny to all at the end, as a catchall to insure that only intended users have access to this controller's actions.

But if no matches are made, Yii defaults to allow, and for many applications this is insecure and dangerous behavior. A developer not paying attention to his rules could find unauthorized users doing unauthorized things.

Better Security = default-deny

Those who are security minded prefer a default-deny approach, because getting rules wrong (accidentally denying behavior you wish to allow) is much more obvious to show up in testing, where it can be fixed. Accidentally allowing behavior you wish to deny is much harder to detect.

Though there is no real substitute for careful coding, for something this important we prefer a more failsafe approach by overriding the access control code to automatically add a default-deny to the list.

The proper place to do this is in your own project-specific class that extends CController and forms the base for your own individual model controllers. Having a common MyController class allows you to insert behavior in common to all controllers, and this technique is described in another wiki article:

Ref: Extending common classes to allow better customization

Our approach is to fetch the current controller's rules() — which are defined in the real controller class for the particular set of actions — and add a default-deny to the list, then process the filters as the original CController code would:

// in protected/components/MyController.php
class MyController extends CController {
 
    // filterAccessControl()
    //  
    //  This replicates the access control module in the base controller and lets us 
    //  do our own special rules that insure we fail closed. 
 
    public function filterAccessControl($filterChain)
    {   
        $rules = $this->accessRules();
 
        // default deny
        $rules[] = array('deny');
 
        $filter = new CAccessControlFilter;
        $filter->setRules( $rules );
        $filter->filter($filterChain);
    }
    ...

Then, each of your controllers should extend from this new MyController class to get these (and potentially other) services.

This method is only called for controllers that define accessControl in the filters, and in fact some may not need it: the SiteController used for login handling can be run by anybody. Since they don't define the accessControl filter, there is no default-deny.

Handling default-allow

Some Controllers don't require any access rules (say, the SiteController used for login handling), and those rules don't define 'accessControl' in the filters() section in the first place. For controllers lithem, this code won't be called at all.

But if a controller really does warrant a default-allow, that's easy enough to achieve directly in the rules() with `

public function rules() {
   return array(
       // other rules here
       array('allow') // default allow
   );
}

Even those not implementing this article's technique would do well to add the default-allow rule even though it would be handled by Yii automatically so that others reading the code would know this was intended behavior.

Important Notes

  1. This code duplicates code from the framework's CController class, and this may change from release to release. It's important to check out new releases to insure that this key security-related functionality has not changed. This code is good for (at least) Yii 1.1.5, 1.1.6 and 1.1.7.
  2. It's still a very good idea to include your own default-deny at the end of the access rules in all your controllers, as this helps document the security behavior your controller expects. This overriding base class is intended as an failsafe, not a shorthand to allow shortcuts in the rules.
  3. Those building their applications from the Blog Tutorial base will find that protected/components/Controller.php is already defined as that intermediate helper class; this means you can use it directly without defining a new one.
  4. There really should be a better way of doing this without duplicating framework code.

Total 3 comments

#16084 report it
nsanden at 2014/01/17 09:21pm
You know this is a wiki

You can fix the default allow yourself. Done.

#14448 report it
aviday at 2013/08/12 01:37pm
Excellent article...

Thanks for this article. Very relevant and important.

I'm not sure if the code in the "default allow" was a typo. If it wasn't a typo, it probably deserves a better explanation.

Instead of: array('deny') // default allow Should this be array('allow') // default allow

#10846 report it
lxvi at 2012/11/27 07:22am
that so-called default allow sure looks like a deny to me!

Thanks for proofreading your article :-P

Leave a comment

Please to leave your comment.

Write new article