Configuring controller access rules to default-deny

You are viewing revision #1 of this wiki article.
This version may not be up to date with the latest version.
You may want to view the differences to the latest version.

next (#2) »

  1. Better Security = default-deny
  2. Handling default-allow
  3. Important Notes

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',  // deny all users
                'users'=>array('*'),
            ),
        );
    }
    ...

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', 'users'=>array('*') );

        $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('deny', 'users'=>array('*')) // 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.
11 0
12 followers
Viewed: 140 107 times
Version: Unknown (update)
Category: How-tos
Written by: Steve Friedl
Last updated by: nsanden
Created on: Apr 4, 2011
Last updated: 10 years ago
Update Article

Revisions

View all history

Related Articles