Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added chapter about Core and Extension security #202

Merged
merged 5 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions docs/security/common-vulnerabilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
sidebar_position: 3
---

Common Vulnerabilities
======================

## Cross-Site-Scripting / XSS
Cross-Site-Scripting issues are by far the most common issues in the Joomla extension ecosystem.

A brief example: imagine a Joomla comment extension that allows users to comment an article with a subject and a text.
Now imagine the following output template for a comment:

```php
<div class="comment">
<h3><?php echo $comment->subject; ?></h3>

<?php echo $comment->text; ?>
</div>
```

Looks straightforward, huh? But now imagine that a user does not use "I love your site" as a comment subject, but ```<script>executeEvilJs()</script>```.
With the output template given above, the JS provided by the user will be outputted as an executable HTML tag and the evil code will be executed in the browser of each and every user visiting the site where that comment is shown - that's a Cross-Site-Scripting vulnerability.

### Prevention
#### Filter/validate the user input
In the example above, the provided subject should be filtered and/or validated to only allow required characters - and it should disallow characters that are needed to create HTML tags, i.e. the `<` and `>` characters.
If the user input can contain HTML markup, the markup itself has to be filtered to make sure it only contains safe markup. See [the chapter about input handling](input-handling) for more information.

#### Escape the output
Unless user generated markup is specifically needed (i.e. because the user can use a WYSIWYG editor) it's highly recommended to escape each and every snippet of user provided content.
Escaping converts HTML markup into plaintext by replacing control-characters like `<` with their HTML entities, i.e. `&lt;`.

To escape user content in Joomla, use the ```echo $this->escape($evilString)``` method when outputting content in component view or ```echo htmlspecialchars($evilString, ENT_QUOTES, 'UTF-8')``` outside of component views.

## SQL injections / SQLi
A SQL injection attack is a type of vulnerability where an attacker is able to manipulate a SQL query by injecting user controlled content.

Learn more about this attack scenario and the prevention in [the chapter about secure DB queries](secure-db-queries).

## Unrestricted file uploads
Uploading user provided files to a webservers is a potentially dangerous task as it exposes multiple attack vectors at once:
* by uploading a dangerous file type (i.e. a PHP file) an attacker might be able to execute code
* an attacker might be able to control the storage path and thereby store files in directories where that shouldn't be possible
* by uploading large files, the webspace might be filled by an attacker quickly, leading to a denial of service

Therefore file uploads must be very carefully implemented. Check the ```canUpload``` method of the ```Joomla\CMS\Helper\MediaHelper``` class as it will help you with that.

## Cross-Site-Request-Forgery / CSRF
CSRF is an attack type where an HTML form on an external, attacker-controlled site is used to perform an attack against a target site.

### Prevention
Learn more about this in the [CSRF chapter](csrf-protection) of this manual.
46 changes: 46 additions & 0 deletions docs/security/csrf-protection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
sidebar_position: 5
---

CSRF Protection
======================
Cross-Site request forgery is an attack type where an HTML form on an external, attacker-controlled site is used to perform an attack against a target site.

Imagine the following scenario:
* you are running a Joomla site as an administrator and are currently logged in
* in the same browser you want to buy a super cheap new TV from an online shop that a stranger has just sent you via email
* you click on the "add to cart"-button of that online shop - however, instead of adding the item to a cart, you end up in the Joomla administration of your site, seeing a "the user has been added"-confirmation message

So, what has just happened? Well, you have been hacked!
The "add to cart"-form in the shop has been manipulated by the attacker:

```html
<form action="https://myjoomlasite.com/administrator/index.php" method="post">
<input type="hidden" name="task" value="user.add" />
<input type="hidden" name="option" value="com_users" />
<input type="hidden" name="jform[username]" value="attackeruser" />
<input type="hidden" name="jform[password]" value="attackerpwd123" />
<input type="submit" value="Add to cart" />
</form>
```

The form's ```action``` attribute points to the victim's site - and therefore the request data of that form is forged across the two sites: a CSRF is happening. In this scenario, the CSRF is used to create a new user for the attacker - and as that request is executed with the permissions of the currently logged in user, the request succeeds.

Luckily, this scenario is theoretical as Joomla prevents CSRF attacks with a simple security measure.

## Prevention
In order to prevent CSRF attacks, a randomly generated string is appended to each and every request that is supposed to perform changes to the site:

```php
<form action="index.php" method="post">
[...]
<?php echo HTMLHelper::_('form.token'); ?>
</form>
```

The ```form.token``` method generates a hidden input element with a random name and "1" as value. The random name is stored in the user session and thereby Joomla can check if it's included in the request after the form has been submitted.
In order to trigger the check, use the ```$this->checkToken();``` method in controller classes or the ```Session::checkToken()``` method outside of controllers.

As the random name is stored in the user session and therefore session-specific, it's impossible for an external attacker to include it in the manipulated form. The CSRF attack will fail.

__Important:__ CSRF tokens are unrelated from permission checks! A passed CSRF check does __not__ prove that a user is logged in, or has specific permissions to execute a given task.
7 changes: 7 additions & 0 deletions docs/security/forms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
sidebar_position: 6
---

Forms & Validations
======================
See the [form validation chapter for further information](../general-concepts/forms/server-side-validation).
41 changes: 41 additions & 0 deletions docs/security/fundamentals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
sidebar_position: 2
---

Fundamentals
============
There are a bunch of very basic guidelines that you should follow in order to develop secure code.

## No 1: All user input is evil

Consider each and every input from a user as evil input. It can't be trusted and always has to be filtered and/or validated on input and needs escaping on output.

Please have in mind that this does not only apply to "obvious" sources of user input, like the POST or GET input, but also other sources that are controlled by the user like:
* cookies
* the REQUEST superglobal
* major parts of the SERVER superglobal as it contains user-controlled input like the URL, the domain or the HTTP method
* HTTP request headers

## No 2: client-side security is no security

Everything that's happening in a client-side context (=in the browser) is user-controlled. That also means that any security measure that's implemented on the client-side is no security measure at all, as the user can always deactivate or manipulate these checks.

This fundamental most importantly applies to:
* (form) validation: browser-based input validation is a UX feature (as it provides instant feedback to the user without a page reload) but not a replacement for server-side validation
* access checks: if you, i.e. output a secret string to the browser but hide it via `display:none`, it's still accessible by the user as removing the display-directive is trivial
* rate-limiting: if you limit the number of requests that a client is allowed to perform with client-side measures (i.e. by disabling a button via JS) that's again no protection at all

## No 3: Don't do your own crypto

Properly implemented cryptography does a fantastic job to keep secret stuff secret. The hard part however is implementing it properly, as often details decide about whether an implementation is indeed secure or not.
Therefore you should not do your own implementations of cryptographic methods or algorithms. Instead, use "off-the-shelf" implementations like the [libsodium](https://www.php.net/manual/de/book.sodium.php) methods available in PHP. These methods have been developed by people that are way smarter than you and are extensively tested and reviewed.

## No 4: Don't do security by obscurity

This rule does not only apply to the actual implementations in your codebase, but also to how you should handle a security issue in your code: once it's patched and the patch has been released, be open about it, notify users about the issue and the patch and outline its criticalness.

## No 5: Reduce your attack surface

Every single line of code that you write can contain a security issue and increases your attack surface. Carefully weight the advantages of a new feature against the maintenance effort and the increased attack surface.

For extension developers that also means that Joomla core classes and features should be used whenever possible instead of writing own implementations, as Joomla's core code is well maintained, tested and has active security coverage by the security team.
7 changes: 7 additions & 0 deletions docs/security/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
sidebar_position: 7
---

Security
===============
This section will teach you how to develop secure code for Joomla core and Joomla extensions.
8 changes: 8 additions & 0 deletions docs/security/input-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
sidebar_position: 3
---

Input Handling
======================

See the [Input chapter for further information](../general-concepts/input).
92 changes: 92 additions & 0 deletions docs/security/secure-db-queries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
---
sidebar_position: 4
---

Secure DB Queries
======================
A SQL injection attack is a type of vulnerability where an attacker is able to manipulate a SQL query by injecting user controlled content.

Consider the following code snippet:

```php
$query = $this->db->getQuery(true);
$username = $user->username;
$newPassword = password_hash($newPassword, PASSWORD_DEFAULT);

$query->update('#__users')->set('password = "' . $newPassword . '"')->where('username = "' . $username . '"');

$this->db->setQuery($query)->execute();
```

The code is supposed to update the password of the currently logged-in user who's identified by his username and the resulting query for the user "foobar" will look like this:

```sql
UPDATE jos_users SET password = "{HASH}" WHERE username = "foobar";
```

Now let's assume that the user chose `foobar" OR username="admin` as his username of choice. That would result in a very different query:

```sql
UPDATE jos_users SET password = "{HASH}" WHERE username = "foobar" OR username="admin";
```

So, the attacker has injected his user controlled commands in the query, resetting not only his password but also the password of the admin user.

### Prevention
#### Use prepared statements
This is the gold standard to prevent SQLi attacks.

The basic principle is simple: instead of integrating the user provided values in the query within the PHP code, query and input values are sent to the DB server in separate calls:

```
Prepared Statements
Query: SELECT foobar FROM bar WHERE foo = ?
Data: [? = 'bar']
```

The basic principle is simple: instead of integrating the user provided values in the query within the PHP code, query and input values are sent to the DB server in separate calls. The DB server will then combine queries and values with each other and take care of escaping and/or quotes where necessary.

So, by separating queries and injected values from each other, an injection becomes impossible.

##### Implementing Prepared Statements through JDatabaseDriver
Implementing prepared statements in Joomla is very simple and is cross-platform:

```php
$query = $this->db->getQuery(true)
->select($this->db->quoteName(array('id', 'password')))
->from($this->db->quoteName('#__users'))
->where($this->db->quoteName('username') . ' = :username')
->bind(':username', $credentials['username']);
```

Within your query you define a so called named placeholder, prefixed with a double-colon. The actual replacement value for that placeholder is then passed to the DB server using the ```bind()``` method.

The following functions accept arrays to reduce function call overhead:
* bind()
* bindArray()
* whereIn()
* whereNotIn()

__!If possible you should use prepared statements!__

Learn more:
* [The PHP Manual on Prepared statements](https://php.net/manual/en/pdo.prepared-statements.php)
* [Simple Pull Request Implementing a Prepared Statement in Joomla](https://github.com/joomla/joomla-cms/pull/25049/files )
* [Joomla Framework API](https://api.joomla.org/framework-1/classes/Joomla.Database.DatabaseQuery.html)


#### Escape user controlled input
By escaping characters that are considered as control-characters in SQL queries, it's also possible to prevent an attacker from "escaping" from the double-quote-prison that you have built in a query:

```php
$query = $this->db->getQuery(true);
$username = $user->username;
$newPassword = password_hash($newPassword, PASSWORD_DEFAULT);

$query->update('#__users')->set('password = "' . $this->db->escape($newPassword) . '"')->where('username = "' . $this->db->escape($username) . '"');

$this->db->setQuery($query)->execute();
```

You can also remove the manually added double quotes in the query and use the ```quote()``` and ```quoteName()``` methods as these will by default also escape the provided input.