Security

From basic principles to PHP specifics

Alexander Makarov

Yii core team

https://slides.rmcreative.ru/2021/phpday-security/

Обо мне

  • 15+ years in IT: Java, PHP, JavaScript etc.
  • Writer, speaker
  • Organizer of PHP Russia, program commitee member of Highload and RIT
  • OpenSource contributor
  • Yii lead and PHP-FIG representative
  • Consultant
  • In the past: Siemens, Wrike, CleverTech, Stay.com, Skyeng

Not a true security expert. Just know a thing or two.

I did code audit for many projects...

... and saw the same issues again and again

Basics are...

Never trust data no matter what

Filter input,
escape output

Input

  • Forms
  • Files
  • HTTP headers ($_SERVER['HTTP_X_FORWARDED_FOR'] etc.)
  • User agent
  • ...

Output

  • Browser
  • Console
  • Database
  • ...

Problems?

  • Insufficient filtering
  • Wrong escaping

Filter?

Filtering is making sure data is valid.

Prefer whitelisting

Input is invalid by default unless proven otherwise.

Use filter_var


$email = filter_var($email, FILTER_VALIDATE_EMAIL);
if ($email === false) {
    // email wasn't valid...
}
// everything's OK

or use libraries and frameworks which are reliable

Escaping?

Making special characters behave like normal characters.

Usually by prefixing with another special character.

Each output has different escaping rules.

Common threats

XSS

A script is injected into the page and is executed in user's browser.

Saw it in most projects I've reviewed.


...
<div>
<?= $_GET['query'] ?>
</div>
...
                    

Instead of alerting it can:

  • Do things from your name such as...
  • Ask your frineds for money
  • Make you say/post things
  • Transfer your funds
  • Send nasty stuff to police
  • ...

Two main types

  • 1st order - executed immediately
  • 2nd order - stored XSS executed later

Solution

Escape output.

Escape HTML

If need just text.


htmlspecialchars($content, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
                    

Sanitize HTML

If HTML needed.

HTMLPurifier (http://htmlpurifier.org/)


$config = HTMLPurifier_Config::createDefault();
$purifier = new HTMLPurifier($config);
$clean_html = $purifier->purify($dirty_html);
                    

Don't save escaped input to DB

Because you may need to edit it and because data in your own DB could not be trusted.

btw., Chromium developers removed XSS-filter...

XSS in admin panel is not less dangerous...

CSRF

Third party website can submit forms to your website on behalf of the user.

Solution

Introduce CSRF-tokens and use TLS/SSL.

CSRF-tokens

  • When a form is displayed, a unique random token is added to it.
  • Same token is written to storage (usually session).
  • When form is submitted, tokens are checked to match.

Same origin policy isn't enough

  • It's about XMLHTTPRequest
  • It doesn't prevent sendingrequest, it prevents getting results

Do not use GET to modify application state


<img src="/logout" />
                    

Using $_REQUEST is the same.

SQL injection

Executing arbitrary SQL on a project database.


$email = $_POST['email'];

query("SELECT *
FROM user
WHERE email = '$email';");
                    

' OR 1
                    

UNION SELECT 1,'',3,4,5 INTO OUTFILE '1.php' --%20
UNION SELECT 1,LOAD_FILE('config.php'),3,4,5 --%20
                    

Solution

Escape queries.

Properly!

Manual escaping is... not really good

  • addslashes()
  • mysql_escape_string()
  • mysql_real_escape_string()
  • ...

Because

  • Could be forgotten
  • Context could be wrong (values, tables, columns)
  • Could be incomplete
  • ...

Use prepared statements

PDO or driver specific. For PDO:


$stmt = $db->prepare("SELECT * FROM user WHERE email = :email");
$stmt->bindValue(':email', $email);
$user = $stmt->fetch();
                    

Table and column names

Use whitelist.


$allowedTables = ['user_comments', 'post_comments'];
if (!in_array($table), $allowedTables, true) {
    return false;
}
// query as usual
                    

Lack of upload validation

Ability to upload and execute code.

  • Validate mime
  • Re-save images

Code injection

Directly execute code.


eval($_GET['query']);

Solution

Avoid eval() or at least use whitelist.

Run PHP with as less permissions possible

Unsafe includes


require $_GET['type'] . '.php';
Whitelist.

Clickjacking

Trick user into actually clicking on third party website.

iframe + opactity: 0

Not really PHP related but quite serious.

Solution

Disable embedding in a frame via RFC 7034:


header('X-Frame-Options: DENY');
// или
header('X-Frame-Options: SAMEORIGIN');
                    
or via JavaScript:

if (window.top !== window.self) {
    document.write = "";
    window.top.location = window.self.location;
    setTimeout(function () {
        document.body.innerHTML = '';
    }, 1);
    window.self.onload = function (evt) {
        document.body.innerHTML = '';
    };
}

Passwords

Problem?

Attacker could guess your password

  • Brute force
  • Dictionary attack
  • Timing attack

Solution: Limit attempts

  • 10 guesses from an IP per minute
  • CAPTCHA

Still a problem?

  • Easy to change IP
  • Database leaks

Does it matter how to store password?

Yes!

You should not know user password!

Save a hash.

md5, sha1, sha256, sha512 etc. are no go

Even with salt.

Hashes are meant to be computed fast

ull brute-forcing of 8-char password hashed as SHA-256 takes...

3.5 days of brute forcing on 2011 single GPU

About 20 hours on 2015 GPU

8x Nvidia GTX 1080 — 2.5 hours

GPU?

Visiting the farm
GPUs are cool!

Use at least bcrypt

  • Meant to be not so fast.
  • Introduces workload for CPU/GPU.
  • GPU unfriendly (requires lots of memory access which spoils parallelism).

Cost and password strength are important

Cost?

key derivation function iterations = 2^cost

12+ is a safe choice. Yii uses 13.

Let's do some calculations

Cost = 13 → ~28 hashes/sec. on Nvidia GTX Titan X ($700, 2015)

28 * 60 * 60 * 24 = 2419200 hashes a day

6 char lowercase letter-only password = 308915776 combinations

308915776 / 2419200 = 127 days to brute-force a single password

21 days with 6 of such cards. Costs more than ~4200$

Add numbers to 6 char password and to break it in 21 days it would cost more than ~22600$

GPUs are getting better and cheaper. If we don't consider crypto-mining.

AMD R380X (2016) gives you 14 bcrypt hashes with cost=13 per second and costs ~$78.

Prepared attacker has way better hardware

bcrypt gives you only time

If you know hashes are leaked:

  • Fix leak source
  • Invalidate hashes
  • Invite users to change passwords and explain that passwords should be changed everywhere

Suggest strong passwords but do not limit them

It would give you more time to react.

You can get plenty of time if you'll use Argon2

Again... these are not for passwords

  • md5, sha1, sha256, sha512
  • PBKDF2

Sessions

  • Fixation - give user a known session ID.
  • Obtain user cookie.

Solution

Use cookies only. In php.ini:

  • session.use_cookies = 1
  • session.use_only_cookies = 1
  • session_cookie_httponly = 1
  • session.cookie_secure = 1

Regenerate session ID with session_regenerate_id(true) after login or permissions change.

Don't forget to check permissions

Yes, it happens.

  • Direct file access via URL.
  • Hiding is not securing.

Prefer denying everything then allowing what's needed.

Information leaks

  • Debug mode
  • Error pages
  • API responses
  • Version control

Password leaks

  • From VSC
  • From files

A fix?

HashiCorp Vault

gitleaks GitHub action

2FA

Have I been pwned

Limit passwords lifetime (Microsoft does not recommend it)

Random numbers

Used for tokens, reset passcodes, generated passwords, UUID, etc.

Problem

Random numbers could be guess-able or could collide if random source isn't good enough.

Tokens, reset passcodes, generated passwords

Solution

Use safe random sources

What Yii 2 uses

Be careful about libraries

Conduct tests

Server stack

Be up to date

  • Linux
  • nginx
  • PHP
  • СУБД
  • Your favorite framework
  • ...

But beware of insecure dependencies...

Or dependencies with vulnerabilities introduced on purpose...

Do not trust... check!

DDOS

  • Plan it in advance.
  • Rely on firewall (possibly hardware one).
  • Could get really complicated.
  • Often that's just stray bots and these could be eliminated with fail2ban or CloudFlare.

Have an experienced admin around...

Btw., admins and support aren't ideal either

  • Wide open memcached and mongodb
  • Windows support: activated Windows without checking
  • hetzner support: rebooted server
  • Give permissions to ssh to prod to anyone within the company
  • Servers are in 2+ data centers, API without passwords
  • Insecure S3-bucket
...

Security is a process

It can't be achieved once and for all.

Educate your team

Have someone familiar with security in the team. Use VCS and do code reviews.

Think about bug bounty

Absolute security is impossible

People are vulnerable

  • Lose flash drives
  • Lose laptops
  • Lose phones
  • Let strangers into office
  • And there...

Plan in advance.

  • What if attacker got access to X?
  • What data should never ever be exposed? (= should not be stored)
  • Should admin take down the server in case of being compromised?
  • What was accessed? How? What's the harm?
  • How to fix that?
  • Explain to team so they won't make such mistake again or find technical means to enforce it.

Never stop learning

  • target="_blank" is unsafe
  • ImageMagick is unsafe
  • SMS are unsafe
  • XML as well! (XXE)
  • Uh-oh, unserialize() isn't safe!
  • CPUs are unsafe :(
  • ...

Make it too expensive to crack

Stay safe

Reading list

Questions time!