Security

Improvements?

Let us know by posting here.

Shared Responsibility

Governments and Fortune 500 companies depend on Concrete, so the robust security of Concrete CMS is always a top priority. While the transparency of open-source code accelerates contributions to security enhancements, it also makes the methodology accessible to potential adversaries. As a result, the Concrete core team adopts an unwavering commitment to address security issues with utmost urgency.

However, this rigorous security foundation is just the beginning. As a developer utilizing Concrete for web applications, you play a pivotal role in maintaining this security paradigm. The core of Concrete is secure, but it's essential to ensure the code you introduce upholds these standards. Thankfully, Concrete provides a suite of helper libraries and functions to aid in crafting secure code, but the onus is on you to leverage them effectively.

Preventing Cross-Site Request Forgery with Tokens

Cross-Site Request Forgery (CSRF) allows a malicious actor to trick users into unknowingly submitting a form, potentially leading to unauthorized actions. As an example, a hacker could exploit a form on a Concrete CMS site to change a user's password.

To illustrate, consider a simple form:

<form method="post" action="<?=$view->action('change_password')?>
    <input type="password" name="new_password" />
    <button type="submit">Change Password</button>
</form>

With the corresponding controller:

public function change_password()
{
    $u = new User();
    if ($u->isRegistered()) {
        $ui = $u->getUserInfoObject();
        $ui->changPassword($this->request->request->get('new_password'));
    }
}

This setup is vulnerable to CSRF. A hacker could use an iframe to send a POST request, changing the password if the user is logged in.

To protect against this, we can use a token:

<form method="post" action="<?=$view->action('change_password')?>
    <?=Core::make('token')->generate('perform_change_password');?>
    <input type="password" name="new_password" />
    <button type="submit">Change Password</button>
</form>

In the controller, we verify the token:

public function change_password()
{
    $u = new User();
    $token = \Core::make("token");
    if ($u->isRegistered() && $token->validate('perform_change_password')) {
        $ui = $u->getUserInfoObject();
        $ui->changPassword($this->request->request->get('new_password'));
    }
}

For Dashboard pages in Concrete, you don’t need to fetch the token with Core::make(). It’s already available:

<form method="post" action="<?=$view->action('change_password')?>
    <?=$token->generate('perform_change_password');?>
    <input type="password" name="new_password" />
    <button type="submit">Change Password</button>
</form>

Even if you're not using a form, you can still include token validation. For instance, a button to remove themes could use a token to ensure security:

$valt = Core::make('token');
$bt = Core::make('helper/concrete/ui');
echo $bt->button(t('Remove Object'), $view->url('/path/to/page', 'remove_object', $valt->generate('remove_object_string')), 'btn-danger');

Remember, always use tokens to secure actions against CSRF attacks. This way, you help ensure the integrity and security of your Concrete CMS application.

Defending Against Cross-Site Scripting: Use Output Filtering & Sanitization

Cross-Site Scripting (XSS) is a major threat. Need to understand XSS? Check this out. The golden rule to counter XSS? Always be skeptical of user input.

Form Input

Displaying user input without checks can expose you to risks. Here's an example with a search form:

<form method="get" action="<?=$view->action('search')?>">
    <input type="text" name="query" />
    <button type="submit">Search</button>
</form>

Controller logic:

public function search()
{
    // ... Some search logic here ...
    $this->set("query", $this->request->query('query'));
}

Display in view:

<h1>Search Results</h1>
You searched for: "<?=$query?>"

Looks okay, right? But, this is prone to XSS. The $query variable is taken at face value without any checks. Entering JavaScript code here could have harmful consequences.

The fix? Use PHP's htmlspecialchars or Concrete CMS's h() function:

$this->set("query", h($this->request->query('query')));

This way, harmful HTML/JavaScript gets neutralized.

Attributes

Be cautious when displaying data, like user profiles, that could have untrusted input. Generally, the best practice is to save raw data, but sanitize it when displaying.

How to display attributes safely:

$ui = \UserInfo::getByName('andrew');
print $ui->getAttribute('bio');

But this might contain unsafe data! Here's how you make sure it's safe:

print $ui->getAttribute('bio', 'displaySanitized');

The getAttribute() function, when passed a second parameter, first tries to sanitize the data before displaying it. For instance, the rich_text attribute will run through:

getDisplaySanitizedValue()

Then, if needed:

getDisplayValue()

If you have attributes, especially from users, make sure to display them using 'displaySanitized'.

Sanitizing User Input

Are you accepting user input and you really need to be sure it comes in in a sanitized format? This can easily be accomplished by using our sanitization libraries:

Basic HTML Special Character Encoding

h($request->request->get('maliciousParameter'));

The POST variable 'maliciousParameter' will be run through htmlspecialchars.

Special Sanitize Methods

Use the sanitizing service to sanitize based on types:

Retrieve the Service

$service = \Core::make('helper/security');

Then you can run all the sanitize methods found in Concrete\Core\Validation\SanitizeService

print $service->sanitizeInt($request->request->get('maliciousInt'));

Or email:

print $service->sanitizeEmail($request->request->get('maliciousInt'));

These methods will only validate integers and emails through.

Preventing SQL Injection

Developers working with databases should be aware of SQL Injection. In short:

An attacker can insert harmful SQL code through your web form or URL. And if you're wondering how they know about your database structure, remember, we're open source!

Concrete's core is fortified against SQL injection, but you must ensure your code is too. Here's an example of vulnerable code:

<form method="post" action="<?=$view->action('search')?>
    User ID: <input type="text" name="uID" />
    <button type="submit">Search</button>
</form>

public function search()
{
    $db = \Database::connection();
    $ids = $db->GetCol('select cID from Pages where uID = {$this->request->request->get('uID')}');
}

A user might input "1 union select uEmail from Users" to fetch email addresses or more.

Solution 1: Use the API

Avoid direct SQL when possible. Use the concrete5 API which is safer:

public function search()
{
    $ui = \UserInfo::getByID($this->request->request->get('uID'));
    if (is_object($ui)) {
        $list = new \Concrete\Core\Page\PageList();
        $list->filterByUserID($ui->getUserID());
        $results = $list->getResults();
    }
}

This approach is more secure and concise.

Solution 2: Apply Placeholders

If you must run a direct SQL query, use placeholders. They're supported by Doctrine DBAL and PHP's PDO:

public function search()
{
    $db = \Database::connection();
    $ids = $db->GetCol('select cID from Pages where uID = ?', array($this->request->request->get('uID')));
}

It's clear and protects against SQL injection.

File Upload Checks

When handling file uploads in custom code, always verify the file first. Here's how to safely validate files:

Use Basic PHP Checks

For any file upload via a form:

<form method="post" enctype="multipart/form-data" action="<?=$view->action('save_avatar')?>">
    <input type="file" name="avatar">
    <button type="submit">Save Avatar</button>
</form>

Ensure to employ standard PHP checks:

public function save_avatar()
{
    if (isset($_FILES['avatar']) && (is_uploaded_file($_FILES['avatar']['tmp_name']))) {
        // ready to continue
    }
}

Confirm It's an Image

Use Concrete's built-in Image validation to ensure the file is an image:

public function save_avatar()
{
    if (isset($_FILES['avatar']) && (is_uploaded_file($_FILES['avatar']['tmp_name']))) {
        $service = \Core::make("helper/validation/file");
        if ($service->image($_FILES['avatar']['tmp_name'])) {
            // It's a confirmed image
        }
    }
}

Check File Extensions

Ensure the file extension is supported:

public function save_avatar()
{
    if (isset($_FILES['avatar']) && (is_uploaded_file($_FILES['avatar']['tmp_name']))) {
        $service = \Core::make("helper/validation/file");
        if ($service->extension($_FILES['avatar']['name'])) {
            // Extension is supported
        }
    }
}

This validation uses the list of allowed extensions from the Concrete Dashboard.

Encryption Service

Need to encrypt potentially sensitive data, while still being able to retrieve it for later? Use the encryption service!

First, retrieve the service:

$encryptor = \Core::make("helper/encryption");

Now, you can use

$encrypted = $encryptor->encrypt('This text will be encrypted.');

And

print $encryptor->decrypt($encrypted); // "This text will be encrypted."

Important Note: These functions rely on the mcrypt library; if the library is not installed, the output will not be encrypted. It will just be passed back as unencrypted text.

Encrypting User Passwords

Note: this is NOT to be used for user passwords! It's not secure enough. In general, you should never encrypt user passwords with anything that can be reversed. Instead, encrypt it using a one-way hashing algorithm, and any time the user enters their password, compare the value of the hashed provided password with the one you're storing. This is all taken care of by Concrete when dealing with the standard User model, but if you need to encrypt user passwords yourself for business purposes, here's the secure, standardized method by which Concrete accomplishes this.

First, retrieve the global password hasher from the User object:

$u = new User();
$hasher = $u->getUserPasswordHasher();

This will return an instance of the Hautelook\Phpass\PasswordHash object, with the proper configuration values set for portability and proper encryption of data. Then, it's a simple matter of either calling

return $hasher->HashPassword($password);

to generate an encrypted password for storage, or

$hasher->checkPassword($inputPassword, $storedPassword)

to check an inputted password against the password stored in the database.

Anti-Spam and Captcha

Spam is a significant issue online. Without protection, online contact forms quickly become inundated with spam. Luckily, there are several systems available to counter this issue. All user-facing forms, like Form and Survey blocks, as well as the registration system, channel their input through our Anti-Spam layer. Furthermore, Captcha can be used with all these forms.

Enabling Anti-Spam and Captcha

For detailed instructions on activating these features, refer to the Captcha and Spam Control sections of the Editors Documentation.

Implementing Captcha in Your Forms

To integrate captcha into your custom form, append this code at the end of the view layer:

<form method="post" action="<?=$view->action('submit_form')?>">
<?php
$captcha = Core::make('captcha');
?>
<div class="form-group">
    <label class="control-label"><?=$captcha->label()?></label>
    <div><?php $captcha->display(); ?></div>
    <div><?php $captcha->showInput(); ?></div>
</div>
</form>

This will show the output of the chosen Captcha library.

Ensure to validate the Captcha when the form is submitted:

public function submit_form()
{
    $captcha = \Core::make("captcha");
    if ($captcha->check()) {
        // Continue with submission
    } else {
        // Inform the user of the captcha error
    }
}

With that, your form now has captcha protection.

Implementing Anti-Spam in Your Forms

To add anti-spam support, consider the example form with input fields first_name, last_name, and feedback. The method below will check these fields for spam:

$antispam = \Core::make('helper/validation/antispam');
$message = "First Name: {$this->request->request->get('first_name')}\n";
$message .= "Last Name: {$this->request->request->get('last_name')}\n";
$message .= "Feedback: {$this->request->request->get('feedback')}\n";
$u = new \User();
$additionalArgs = array('user' => $u);
if (!$antispam->check($message, 'feedback_form', $additionalArgs)) {
    return false;
}

Creating Your Own Captcha Service

The Anti-Spam and Captcha services can be customized. Initially, there are no Anti-Spam services and a basic SecurImage Captcha is provided. However, you can easily design your own Captcha and Anti-Spam libraries as part of a Package.

Setting Up Your Captcha

During your package's install() method, integrate your Captcha library. Assuming your package is called "Awesome Captcha" with the handle "awesome_captcha":

public function install()
{
    $pkg = parent::install();
    \Concrete\Core\Captcha\Library::add('awesome_captcha', t('Awesome Captcha'), $pkg);
}

Designing a Captcha Class

When setting up your class, remember to provide the essential methods: showInput(), display(), label(), and check().

Custom Options

If your Captcha needs custom settings, like an API key for Google's ReCaptcha, you can define these in the Dashboard. Create a form at packages/awesome_captcha/elements/system/captcha/awesome_captcha/form.php, and then specify a saveOptions($data) method in your controller to store this information.

Crafting Your Own Anti-Spam Provider

Setting Up Your Provider

In your package's install() method, integrate your Anti-Spam library. For a package named "Akismet" with the handle "akismet":

public function install()
{
    $pkg = parent::install();
    \Concrete\Core\Antispam\Library::add('akismet', t('Akismet'), $pkg);
}

Designing an Anti-Spam Class

Ensure that your class implements the necessary methods: check($data) and report($data).

Custom Options

For custom settings, create a form at packages/akismet/elements/system/antispam/akismet/form.php. Then, define a saveOptions($data) method in your controller to store the data.

Programmatically Managing IP Blacklist

The IP Blacklist is a robust feature that is always active when the Anti-Spam system is in use. You can interact with the IP blacklist through code as follows:

Verifying an IP

To check an IP, first get the IP Service:

$ip = \Core::make('helper/validation/ip');

Then, execute the check:

if ($ip->isBanned()) {
    // IP is blacklisted
}

Blocking an IP

To blacklist the current IP:

$ip->createIPBan();