Adoption of Rule #4 from AppSec Manifesto

Alex Tatulchenkov
5 min readMay 19, 2021

Introduction

This is the last article in the series of articles about the adoption of the AppSec Manifesto. In the previous article, we discussed how one can adopt Rules #2 and #3

RULE #4

Declaration of Sources Rights

All sources are born at the same architectural level and should be treated equally.

If we look at the Hexagonal Architecture

Hexagonal architecture herbertograca.com

or at Clean Architecture

Clean Architecture (Uncle Bob)

We can see that all Sources (the program point that reads external resource) belong to the same architectural layer (level)

Assume we have a process of user registration. Data may come from different sources:

  • From the Web (a user sends registration form)
  • From CLI ( php register.php -u demo_user@example.com -p secret)
  • From SOAP or REST call
  • etc

In all scenarios, you have to apply the same validation rules for incoming data. In most cases, you already do it if you follow the DRY principle. So you’ll have a Service (UseCase, Command Handler) where you do all needed operations if provided input conforms to the corresponding rules. Let's say the username must be an email no longer than 32 characters and the password is a string from 8 to 20 characters.

But usually, people don’t apply the same rules for the same type of data when these data come from the database. Assume you created a new user. Will you validate his username again when you read the corresponding record from the database? If you don’t use custom data types I doubt you’ll do that. Most people assume that if data was validated before putting it into the database, they may trust it (treat it as valid).

Imagine a situation when data was changed in the database via another application, or by DBA directly in the database, or business rules have been changed e.g. password minimal length should be 10 characters. Should your application fail when you try to retrieve a user record? If you follow RULE #4 and use custom data types (ValueObjects) then the only possible answer is “Yes”. Let’s also discuss not a theoretical but the real-life scenario: one company stored customers' email addresses in the MySQL database that was configured so that a value for a column that exceeds the column’s maximum length will be truncated to fit the length. They realized that some of the addresses have been truncated only when got notified by email services they used — email can’t be delivered because the email address is invalid. And this was the only channel to reach the customer. If they follow RULE #4 they would get Exception in any scenario related to user management. It is fair to say that in this example there may be edge cases: e.g foobar@examle.com is valid and foobar@example.co is also valid, so the value stays valid even after truncation, and to prevent such issues you need a digital signature (authentication + integrity) at least for critical data.

There is a hint in AppSec Manifesto that should be mentioned here:

Quod licet Iovi, non licet bovi

Throw exceptions (fail hard) for interactive input, degrade gracefully for non-interactive one

While you should apply the same validation rules to the same user input regardless of the source you may react to rules violations differently.

Going back to the example with registration:

  • If you have a form on your website and you validate multiple fields it’s OK if you violate RULE #2 and check all the fields instead of throwing an exception of the first error to provide the user a better UX.
  • If it’s an API call then falling hard is probably a better option

Both cases are interactive input — the “author” of the data receives the response (error message) and in the case of fail, we can “ask” the user to correct his input and resend. But what if we have a page with the list of the user’s email addresses in the application and one or a few of them violate validation rules? This is non-interactive input and there is nobody to ask. And this is up to you how to react to such a situation: ERROR 500?

I mentioned that you may violate RULE #2 on interactive input but if you use ValueObjects it’s not looking possible. So it looks like we need to validate data before we instantiate ValueObjects using the same validation rules as ValueObject. But it seems to violate a DRY principle as well. What can be done in this situation? We can combine Try-Parse Pattern and Notification Pattern.

I’ve created a repo on GitHub with one of the possible implementations.

Your ValueObject needs to implement the following interface

public static function tryParse($value, ?Typed &$typed = null, ?ValidationResult &$result = null): bool;

and you need a class that follows the Notification pattern.

To simplify the creation of such ValueObjects typedvalue library provides a TypedValue trait so you may use it as:

class StringValue implements Typed
{
use TypedValue;
public static function validate($value): ValidationResult
{
$result = new ValidationResult();
if (!is_string($value) || strlen($value) <= 0) {
$result->addError('Only not empty strings are allowed');
}
return $result;
}
}

ValidationResult here implements the Notification pattern while TypedValue does the heavy lifting

public static function tryParse($value, ?Typed &$typed = null, ?ValidationResult &$result = null): bool
{
... // omitted
$result = static::validate($value);
if($result->fails()) {
return false;
}
$typed = new static();
$typed->value = $value;
return true;
}

Now you may do things like this:

// single param
if (StringValue::tryParse('foo')) {
echo 'Foo is a valid input';
}
// two params
if (StringValue::tryParse('foo', $typed)) {
// you have an instance of StingValue
echo $typed->toNative(); // foo
}
// three params
if (!StringValue::tryParse(false, $typed, $result)) {
// ValidationResult $result contains all the errors
print_r($result->getErrors());
}

Also, we may notice that a form is just a composition of fields and we may use a Composite pattern to represent a form as a composition of corresponding ValueObjects. Of course, typedvalue lib get you covered here as well. It provides CompositeValue trait

final class Form implements Typed
{
use \dzentota\TypedValue\CompositeValue;
private Email $email;
private Url $url;
private Option $option;
}
class Option implements Typed
{
use TypedValue;
public static function validate($value): ValidationResult
{
$validation = new ValidationResult();
if (!($value === null || is_string($value))) {
$validation->addError('Only strings allowed');
}
return $validation;
}
}
class Email implements Typed
{
use TypedValue;
public static function validate($value): ValidationResult
{
$validation = new ValidationResult();
if (false === filter_var($value, FILTER_VALIDATE_EMAIL)) {
$validation->addError('Not a valid email');
}
return $validation;
}
}
class Url implements Typed
{
use TypedValue;
public static function validate($value): ValidationResult
{
$validation = new ValidationResult();
if (false === filter_var($value, FILTER_VALIDATE_URL)) {
$validation->addError('Not a valid url');
}
return $validation;
}
}

So you may use Form in the same fashion as you did with single ValueObjects

(PHPUnit test example)

$native = [
'email' => 'foo@bar.com',
'url' => 'https://example.com'
];
$expected = [
'email' => 'foo@bar.com',
'url' => 'https://example.com',
'option' => null
];
$isParsed = Form::tryParse($native, $composite);
$this->assertTrue($isParsed);
$this->assertInstanceOf(Form::class, $composite);
$this->assertEquals($expected, $composite->toNative());

Instead of conclusion

AviD’s Rule of Usability:

Security at the expense of usability comes at the expense of security.

--

--

Alex Tatulchenkov

Senior Software Engineer at Intetics Inc., AppSec Manifesto evangelist