Totally Communications
Published 26th January 2018.
Totally, or Totally Communications Limited (03879646) at Unit 801 Highgate Studios, 53-79 Highgate Road, London, NW5 1TL; is a company that has failed to understand and address basic security, accessibility, and performance issues.
These are the three key areas when creating websites.
I appreciate mistakes happen, but below are some of the issues that were identified when working with them.
When I first met their lead developer, I was impressed to see the abbreviation CSRF on a white-board. I assumed this was due to training junior developers on some of the issues they need to be aware of.
Unfortunately I later found out that Totally did not protect against CSRF attacks, so I can only assume the "training" didn't work that well.
For example, if the admin was logged in, and found themselves on a malicious webpage (or was shown a malicious advert), the attacker is able create a new account on the target website - they just create a simple form that auto-submits to /admin/users/add/.
Their first penetration test was by a third party company on the 12th October 2015, it identified a few issues, such as XSS.
Totally incorrectly "fixed" this by using the following:
// stripTags for XSS prevention
$filter = new StripTags();
$this->firstName = $filter->filter($this->firstName);
$this->lastName = $filter->filter($this->lastName);
$this->email = $filter->filter($this->email);
The Zend documentation has a clear warning that StripTags must not be used to prevent XSS attacks.
They also only applied this "fix" in 2 locations:
- Admin/User.php:132
- Resource/Resource.php:145
All other items on the website were ignored.
Further calls for them to fix it properly were either ignored, or they reported that they had fixed it (again) - The second penetration test on the 11th April 2017 confirmed that they had not.
This was eventually reported to the ICO on the 7th March 2017, who decided to take no further action on the 5th September 2017, as my personal data was not effected. It was suggested that publishing details might prompt individuals (data subjects) to raise concerns directly with the ICO. But I don't believe publication will be helpful in this case.
The solutions to protect against XSS are well known (e.g. templating systems, encoding values), but it's still possible to make mistakes. Which is why browsers provide a couple of mitigations to help limit the damage. Unfortunately Totally had done neither:
- Mark the session cookies as "httpOnly". A very simple change, which protects the session cookie from being stolen. While this was mentioned in the first penetration test, they finally did this on the 2nd March 2017.
- Implement a Content Security Policy, to block untrusted JavaScript. This wont be easy for Totally, as they use inline JavaScript (not ideal); so they would need to either use "unsafe-inline" (there is a clue in the name), or rewrite large amounts of their code.
Their SQL included things like:
$courseId = $_REQUEST['courseId'];
if (!empty($courseId)) {
$where_sql[] = 'ct.course_id = ' . $courseId;
}
if (!empty($keyword)) {
$sql .= ' AND uo.name LIKE "%' . $keyword . '%"';
}
Which are perfect examples of SQL injection vulnerabilities, allowing for a simple UNION to return all of the data from the database.
Backups were being created with a very broken script:
if ($tm->mday == 1) {
print "**FIRST OF THE MONTH** - creating new snapshot\n\n";
...
`$mysqldump $db > $backup_dir/$db.sql`;
...
}
...
# rsync any binary log files
print "**SYNCING BINARY LOGS**\n\n";
$out = `$rsync --exclude=*.index $bin_log_dir/$bin_log_prefix.* $backup_dir/`;
Source: backup_db
The binary log was disabled in this case, but even if it wasn't, it would typically contain less then a months worth of data. And even if it did, you can't use that binary log file if it's not synchronised with the dump file (more info).
When it comes to storing passwords, they use SHA256:
class User extends \Ovoyo\Application\User
{
...
const HASH_TYPE = 'sha256';
...
public function init()
{
...
$this->setHashType(self::HASH_TYPE);
...
public function setHashType($hashType)
{
$this->_hashType = $hashType;
}
...
public function login($email, $password, $sessionIdentifier, $ignoreHash = false) {
...
if (isset($this->_hashType) && !$ignoreHash) {
$password = hash($this->_hashType, $password);
}
$fetchBy = array('email' => $email, 'password' => $password);
...
if (!$this->fetchBy($fetchBy)) {
$error = 'You provided an incorrect email address and password';
Ovoyo_ErrorHandler::addError($error);
return false;
}
Source: User.php
Passwords should use a hashing algorithm that is designed for storing passwords, e.g. bcrypt, scrypt, PBKDF2, Argon2.
Doing this in PHP is very easy - you just need to use the password_hash function to create a value that's stored in the database. Then, during login, you verify it with password_verify. For bonus points there is a function you can use to see if the password needs to be rehashed.
They often used the password "t0ta11y" - e.g. for MySQL.
And when they created a login process that involved a WordPress website, they forwarded the users login details to an API, with the users password being sent via a GET request - so the access logs included the passwords in plain text.
Server admin involved logging in via their own account, which was good to see. But then using "sudo su - www-data", because "the code is owned by www-data user".
Skipping over the "sudo su" thing, you don't want your source code to be owned by the account that also runs the web server - If someone malicious was able to write to arbitrary file locations, you don't want them to edit the websites source code.
The web server account should only be able to write to certain folders (e.g. user uploaded files), and those folders would ideally disable the PHP engine.
Live customer data was used on development and testing servers.
Old SQL dump files were found in the developers home directories.
They generate their session ID's and password reset tokens with two functions like this:
$sessionId = '';
srand((double) microtime() * 1000000);
for ($n = 0; $n < 64; $n++) {
$varchar = rand(1,3);
switch ($varchar) {
case 1: $sessionId.= chr(rand(97,122));
break;
case 2: $sessionId.= chr(rand(48,57));
break;
case 3: $sessionId.= chr(rand(65,90));
break;
}
}
Source: UserSession.php and User.php
So these "random" values are simply based on the current time.
This allows anyone malicious to dramatically improve their chance at being able to guess these "secret" values.
For example, someone malicious could request a new password, record the "Date" header in the response, then take a few guesses as to what that secret token would be.
From an accessibility point of view:
They used labels with no for attribute:
<section>
<label class="label">Password</label>
<label class="input">
<i class="icon-append fa fa-lock"></i>
<input type="password" name="pass">
</label>
</section>
Randomly included tabindex attributes on a few (4?) page elements, which creates a confusing tab order; and sometimes used placeholders for labels:
<input name='input_1' type='text' tabindex='49' placeholder='Email' ... />
A search "button" being provided by an icon font, so screen-readers can't see it; and without using tabindex="0", so keyboard users cannot access it:
<i class="fa fa-search fa-rotate-90"></i>
And white text over light images (poor contrast); which became more problematic when the images hadn't loaded yet, as they used an off white background colour (#F6F7F8).
From a performance point of view:
Their fairly simple web pages took a while to load. At a basic check, they come in at 3.9MB, 2.0MB, 4.9MB etc, which is odd when they typically only include 1 to 3 "hero" images. Some of this can be explained by large images (e.g. 1500x1000px at 965KB) being shown as thumbnails, and 526KB of minified JavaScript.
Then there is the interesting approach to slow database queries. On a server with an average load less than 0.1, they moved to Amazon RDS for "performance improvements". While RDS has several advantages, and in some cases it can improve performance, in this case it just added network latency, especially when using the public IP address (without an encrypted connection). A slow database query in this situation is more likely to need specific optimisation, indexes, etc.