Blog post

Codoforum 4.8.7: Critical Code Vulnerabilities Explained

Dennis Brinkrolf photo

Dennis Brinkrolf

Security Researcher

10 min read

  • Security
Image shows various elements of code security, languages and bugs

In the SonarSource R&D team we are equally driven by studying and understanding real-world vulnerabilities, then by helping the open-source community secure their projects. This recently led us to uncover and report multiple security vulnerabilities in Codoforum, an open source forum software developed in PHP. The vulnerabilities enable different attack vectors for a complete take over of any Codoforum board with version <4.9 and are rated as critical. No prior knowledge or privileges are required by a remote attacker. We reported all issues responsibly to the affected vendor who released a security patch immediately.

In this blog post we analyze the technical root cause of three vulnerabilities, what security measures were found and bypassed, and how to correctly prevent these in your code. We will look at the vulnerabilities from an attacker’s perspective and demonstrate how various exploitation techniques are used in an attack to sharpen your defender’s mindset.

SQL Injection (CVE-2020-13873)

We found two SQL Injection vulnerabilities and one of these can be exploited as an unauthenticated forum user to extract data from the database. These allow an attacker to fully compromise an administrator account by retrieving a password reset token. Once an administrator account is accessed, the attacker can gain Remote Code Execution on the targeted web server and compromise the system’s host and data.

For demonstration purposes we’ve created a short video that shows the most critical SQL injection vulnerability and its impact.

Technical Analysis

The vulnerability hides within the API call for fetching forum posts. Its code is defined in the routes.php file. Here, a dispatch function maps a route to a function that processes certain URL parameters. As shown in line 165, a topic ID $tid (:tid) is processed from the route that can be modified by a malicious user.


165 function dispatch_get('Ajax/topic/:tid/:from/get_posts', function ($tid, $from) {
// ⋮
168    $topic = new \CODOF\Forum\Topic(\DB::getPDO());
169    $topic_info = $topic->get_topic_info($tid);
179 }

This route has neither CSRF protection nor a permission check and can be accessed from any visitor without authentication. The user input $tid is then passed to the get_topic_info() function without any sanitization.

In the get_topic_info() function, the user controlled variable $tid is concatenated directly into a SQL query in line 462 which is executed in line 464. This is a textbook SQL injection that allows an attacker to malform the SQL query in order to access other SQL tables and columns than intended. Erroneously, the developer assumed that the parameter $tid is an integer before it is included into the query as we can see from the comment in line 461.


458 public function get_topic_info($tid) {
461    // $tid is converted to integer so its safe
462    $qry = "SELECT t.redirect_to,t.topic_id,t.post_id, t.no_posts, t.no_views,t.uid," . "t.title, c.cat_name,t.post_id, c.cat_alias, c.cat_id," . "t.topic_created, t.topic_updated, t.topic_status " . "FROM codo_topics AS t " . "INNER JOIN codo_categories AS c ON c.cat_id=t.cat_id " . "WHERE t.topic_id=$tid AND t.topic_status<>0 LIMIT 1 OFFSET 0";
464    $res = $this->db->query($qry);

Although the SQL injection is easily triggered via the get_posts route, the challenge for an attacker is that there is no access to the SQL query’s result (blind SQL injection). Worst-case, an attacker would need to extract data character by character by using timing techniques. However, there is the possibility to extract the result via an uncaught PDOException because error reporting is enabled by default in Codoforum. This requires less HTTP requests and the data can be extracted in chunks.

The MySQL function extractvalue() can be abused during a SQL injection attack for this purpose. It constructs an XPath query and checks for correct syntax. When we define a faulty XPath that includes information that we want to read, e.g. the MySQL version number, then this is leaked as part of the error message.


With the help of this error-based technique, an attacker can extract data from the database quickly and efficiently. For example, the attacker could extract all passwords from the users table. This is very inefficient though because Codoforum stores only the hashes of all passwords using the bcrypt algorithm. The attacker would need to make the effort of cracking these hashes in order to login.

There is a more clever way. By requesting a password reset for a user, for example the forum’s administrator, a password reset token is generated and stored in the database. Although the attacker does not have access to the admin’s email to receive this token, he can now abuse the SQL injection to extract that token directly from the database. As a result, the attacker can reset the admin’s password with that token and then login as administrator. From here, the attacker can abuse administrator features to compromise the server as we will see in the last section of this post.


By using prepared statements or an integer typecast it is prevented that an attacker can inject arbitrary SQL syntax and mix user input with the SQL query. This way, the attacker cannot modify the SQL query to its advantage anymore.


458  public function get_topic_info($tid) {
461     $tid = (int)$tid;
462     $qry = "SELECT t.redirect_to,t.topic_id,t.post_id, t.no_posts, t.no_views,t.uid," . "t.title, c.cat_name,t.post_id, c.cat_alias, c.cat_id," . "t.topic_created, t.topic_updated, t.topic_status " . "FROM codo_topics AS t " . "INNER JOIN codo_categories AS c ON c.cat_id=t.cat_id " . "WHERE t.topic_id=$tid AND t.topic_status<>0 LIMIT 1 OFFSET 0";
464     $res = $this->db->query($qry);

But there were alternative ways for an attacker to compromise the Codoforum board software.

Path Traversal (CVE-2020-13874)

The second vulnerability type found was a Path Traversal that allows an unauthenticated attacker to download arbitrary files from the server, such as sensitive configuration files. Although the developers tried to prevent this vulnerability with input sanitization, the filter could be bypassed. Let’s have a look at the details.

Technical Details

The vulnerability resides in the file attachment feature of the forum. Via the route serve/attachment the attachment() function is called as shown in the code below. Here, in line 64, it calls the constructor Serve(). Note that there are several routes that lead to the vulnerable Serve() function and this route can be used as an unauthenticated user.


62  dispatch_get('serve/attachment', function () {
64     $serve = new \Controller\Serve();
65     $serve->attachment();
66  });

We now inspect what happens in this serve() function. In line 37, a user controlled input $_GET['path'] is retrieved and sanitized. It is concatenated with other strings and then used as a file path in line 42 to open a file that is offered for download. The whole security is based on the sanitize() function in line 37 which is supposed to prevent a path traversal attack.


35  private function serve($path) {
37     $name = $this->sanitize($_GET['path']);
38     $dir = DATA_PATH . $path;
40     $path = $this->setBasicheaders($name, $dir);
41     header('Content-Disposition: attachment; filename="' . $this->getRealFileName($name) . '"');
42     @readfile($path);
43     exit;
44  }

The following code listing shows the sanitization approach. In line 123, the characters .. are removed and then the url encoded representation %2e%2e is also removed in the next line.


121  private function sanitize($name) {
123     $name = str_replace("..", "", $name);
124     $name = str_replace("%2e%2e", "", $name);
126     return $name;
127  }

The problem is that the PHP function str_replace() does not replace the string recursively and is only processed once from left to right. This means that if the variable $name contains something like /.%2e%2e./ it will be replaced back to /../ and the sanitization is bypassed.

Thus a path traversal attack is possible and an unauthenticated attacker can read arbitrary files from the server by traversing in the file system and accessing sensitive files (…/…/…/other/path/file). This can lead to a full takeover of certain servers hosted with Codoforum.

The faulty sanitization can be fixed by first using urldecode() and then using str_replace("..") or by removing the second replacement altogether.

Persistent Cross-Site Scripting (CVE-2020-13876)

Last but not least, we uncovered a Persistent XSS vulnerability in Codoforum. It enables a low privileged, malicious user to inject a JavaScript payload into the admin backend. When an admin then visits an infected user profile, the XSS payload is executed and the attacker can perform any action as authenticated admin, including the execution of arbitrary code on the targeted web server.

Let’s have a look at how this works. When registering a new user, we can specify an e-mail address which is then reflected in the admin backend. In the following code snippet, in line 679, the registration process is initialized.


675  dispatch_post('/user/register', function () {
677     if (Request::valid($_POST['token'])) {
678        $user = new \Controller\user();
679        $user->register(true);
681        CODOF\Smarty\Layout::load($user->view, $user->css_files, $user->js_files);
682     }
683  });

The following code shows the simplified register() function. Here, in line 3, the user input $_REQUEST['mail'] is retrieved and checked with other information in line 4. If there is no error, such as an invalid or already existing email address, the user will be registered.


1  public function register($do) {
3     $register->mail = $_REQUEST['mail'];
4     $errors = $register->get_errors();
6     if (empty($errors)) {
7        //register user
8     }

For this purpose, the user controlled $mail variable is checked with the PHP built-in function filter_var() and its filter option FILTER_VADLIDATE_EMAIL in line 108. If $mail is a valid email and does not exist, the registration will work without problems (see above).

According to the PHP documentation, the FILTER_VADLIDATE_EMAIL generally validates the email address against the syntax defined in RFC 822. Hence, malicious HTML characters that can be used to construct a JavaScript payload can be used within an email address. Something like "><script>alert(1)</script> is valid and will not be rejected by the filter.


105  public function mail($mail) {
107     $errors = array();
108     if (!filter_var($mail, FILTER_VALIDATE_EMAIL)) {
109        $errors[] = _t("email address not formatted correctly");
110     }
112    if (\CODOF\User\User::mailExists($mail)) {
113       $errors[] = _t("email address is already registered");
114    }
116     $this->errors = array_merge($errors, $this->errors);


50  Email:<br>
51  <input type="text" name="email"  value="{$user.mail}" class="form-control" placeholder=""      required />
52  <br/> 

Since Codoforum uses the PHP template engine Smarty, the escape modifier of Smarty can be used as a patch by replacing line 51 with the following content:

51  <input type="text" name="email"  value="{$user.mail|escape:'html'}" class="form-control" placeholder="" required />

If an admin visits the user profile to edit (block/delete) our registered user, the XSS payload is rendered in the admin’s web browser and we can perform any action as admin on the page. For example, administrator features can be abused to upload a PHP shell and to execute arbitrary code on the server. A similar XSS issue was found earlier that affected the user name.

As a result, an attacker can smuggle an XSS payload within the email address of a new user which is reflected unfiltered in the HTML response of the admin backend.


In this blog post we analyzed three different security vulnerabilities in Codoforum, a popular board software. Each of these issues could lead to a complete takeover of the application. We’ve learned that a malicious user can take multiple paths when attacking an application and that finding only one single vulnerability is enough to fully compromise its security. Hence it is our task to make our applications as robust and secure as possible. Checking all user inputs properly and leveraging existing and proven sanitization and validation mechanisms is the first step towards a solid defense.

Due to the severity of the issues we’ve postponed the release of our blog post since the release of a fix in March. If you are hosting a Codoforum and didn’t update your installation yet, we highly recommend to do so now. Thanks to the Codoforum team, a patch version was quickly released after our reports.

You can join the discussion about this vulnerability in our community forum.