Security best practices

Below we'll review common security principles and describe how to avoid threats when developing applications using Yii.

Basic principles

There are two main principles when it comes to security no matter which application is being developed:

  1. Filter input.
  2. Escape output.

Filter input

Filter input means that input should never be considered safe and you should always check if the value you've got is actually among allowed ones i.e. if we know that sorting could be done by three fields title, created_at and status and the field could be supplied via used input it's better to check the value we've got right where we're receiving it. In terms of basic PHP that would look like the following:

$sortBy = $_GET['sort'];
if (!in_array($sortBy, ['title', 'created_at', 'status'])) {
    throw new Exception('Invalid sort value.');
}

In Yii, most probably you'll use form validation to do alike checks.

Escape output

Escape output means that depending on context where we're using data it should be escaped i.e. in context of HTML you should escape <, > and alike special characters. In context of JavaScript or SQL it will be different set of characters. Since it's error-prone to escape everything manually Yii provides various tools to perform escaping for different contexts.

Avoiding SQL injections

SQL injection happens when query text is formed by concatenating unescaped strings such as the following:

$username = $_GET['username'];
$sql = "SELECT * FROM user WHERE username = '$username'";

Instead of supplying correct username attacker could give your applications something like '; DROP TABLE user; --. Resulting SQL will be the following:

SELECT * FROM user WHERE username = ''; DROP TABLE user; --'

This is valid query that will search for users with empty username and then will drop user table most probably resulting in broken website and data loss (you've set up regular backups, right?).

In Yii most of database querying happens via Active Record which properly uses PDO prepared statements internally. In case of prepared statements it's not possible to manipulate query as was demonstrated above.

Still, sometimes you need raw queries or query builder. In this case you should use safe ways of passing data. If data is used for column values it's preferred to use prepared statements:

// query builder
$userIDs = (new Query())
    ->select('id')
    ->from('user')
    ->where('status=:status', [':status' => $status])
    ->all();

// DAO
$userIDs = $connection
    ->createCommand('SELECT id FROM user where status=:status')
    ->bindValues([':status' => $status])
    ->queryColumn();

If data is used to specify column names or table names the best thing to do is to allow only predefined set of values:

function actionList($orderBy = null)
{
    if (!in_array($orderBy, ['name', 'status'])) {
        throw new BadRequestHttpException('Only name and status are allowed to order by.')
    }

    // ...
}

In case it's not possible, table and column names should be escaped. Yii has special syntax for such escaping which allows doing it the same way for all databases it supports:

$sql = "SELECT COUNT([[$column]]) FROM {{table}}";
$rowCount = $connection->createCommand($sql)->queryScalar();

You can get details about the syntax in Quoting Table and Column Names.

Avoiding XSS

XSS or cross-site scripting happens when output isn't escaped properly when outputting HTML to the browser. For example, if user can enter his name and instead of Alexander he enters <script>alert('Hello!');</script>, every page that outputs user name without escaping it will execute JavaScript alert('Hello!'); resulting in alert box popping up in a browser. Depending on website instead of innocent alert such script could send messages using your name or even perform bank transactions.

Avoiding XSS is quite easy in Yii. There are generally two cases:

  1. You want data to be outputted as plain text.
  2. You want data to be outputted as HTML.

If all you need is plain text then escaping is as easy as the following:

<?= \yii\helpers\Html::encode($username) ?>

If it should be HTML we could get some help from HtmlPurifier:

<?= \yii\helpers\HtmlPurifier::process($description) ?>

Note that HtmlPurifier processing is quite heavy so consider adding caching.

Avoiding CSRF

TBD: what's CSRF, how it works, intro

  1. Follow HTTP specification i.e. GET should not change application state.
  2. Keep Yii CSRF protection enabled.

TBD: how CSRF protection works

Avoiding file exposure

By default server webroot is meant to be pointed to web directory where index.php is. In case of shared hosting environments it could be impossible to achieve so we'll end up with all the code, configs and logs in server webroot.

If it's the case don't forget to deny access to everything except web. If it can't be done consider hosting your application elsewhere.

Avoiding debug info and tools at production

In debug mode Yii shows quite verbose errors which are certainly helpful for development. The thing is that these verbose errors are handy for attacker as well since these could reveal database structure, configuration values and parts of your code. Never run production applications with YII_DEBUG set to true in your index.php.

You should never enalble Gii at production. It could be used to get information about database structure, code and to simply rewrite code with what's generated by Gii.

Debug toolbar should be avoided at production unless really necessary. It exposes all the application and config details possible. If you absolutely need it check twice that access is proerly restricted to your IP only.