Let’s write secure Drupal code!
Balazs Janos Tatar
DrupalCamp London 2019
Who am I?
Tatar Balazs Janos
@tatarbj
Hungarian, lives in Brussels
Works with Drupal since 2007
CTO @ Petend
Drupal Security Correspondent @ EU Commission
Provisional Member of Drupal Security Team
Are there site builders?
Demo
Gist
https://gist.github.com/tatarbj/cc8e96ae328a20b3c9a878eb76e26f37
Are there developers/maintainers?
Have you attended on a previous
Let’s write secure Drupal code!
session?
DrupalCamp Antwerp 2017
DrupalCamp Ruhr 2018
DrupalDevDays 2018
Drupal Europe 2018
DrupalCamp Oslo 2018
DrupalCamp London 2019
(Drupal Mountain Camp 2019)
(DrupalCamp Belarus 2019)
History
Trends in Security
Types of vulnerabilities
Cross Site Scripting
Client side vulnerability
Unfiltered output
Never trust any user input.
We’ve seen the demo before ;)
Cross Site Scripting
Html::escape() – plain text
Xss::filter() – html is allowed
Xss::filterAdmin() – text by admins
Test
Raise your green card if snippet is secure!
Raise your red card if code has issues!
function custom_field_formatter_view(...) {
foreach ($items as $key => $value) {
//...
$element[$key] = array(
'#type' => 'markup',
'#markup' => t('<img src="!src" alt="@alt" />',
array('!src' => $value['src'], ‚$alt’ => $value['alt'])),
);
//...
}
return $element;
}
function custom_field_formatter_view(...) {
foreach ($items as $key => $value) {
//...
$element[$key] = array(
'#type' => 'markup',
'#markup' => t('<img src="!src" alt="@alt" />',
array('!src' => $value['src'], ‚$alt’ => $value['alt'])),
);
//...
}
return $element;
}
function custom_field_formatter_view(...) {
foreach ($items as $key => $value) {
//...
$element[$key] = array(
'#type' => 'markup',
'#markup' => t('<img src="@src" alt="@alt" />',
array('@src' => $value['src'], ‚$alt’ => $value['alt'])),
);
//...
}
return $element;
}
<?php print '<a href="/' . check_url($url) . '">'; ?>
<?php print '<a href="/' . check_url($url) . '">'; ?>
foreach ($items as $delta => $item) {
$id = $item->getValue()['target_id'];
$content = Drupal::entityTypeManager()
->getStorage($entity_type_id)
->load($id);
$body = $content->get('body_field')->getValue()[0]['value'];
}
$elements[$delta] = array(
'#theme' => 'something_custom',
'#body' => $body,
);
return $elements;
foreach ($items as $delta => $item) {
$id = $item->getValue()['target_id'];
$content = Drupal::entityTypeManager()
->getStorage($entity_type_id)
->load($id);
$body = $content->get('body_field')->getValue()[0]['value'];
}
$elements[$delta] = array(
'#theme' => 'something_custom',
'#body' => $body,
);
return $elements;
foreach ($items as $delta => $item) {
$id = $item->getValue()['target_id'];
$content = Drupal::entityTypeManager()
->getStorage($entity_type_id)
->load($id);
$body = [
'#type' => 'processed_text',
'#text' => $content->get('body_field')->getValue()[0]['value'],
'#format' => $content->get('body_field')->getValue()[0]['format'], ];
}
$elements[$delta] = array(
'#theme' => 'something_custom',
'#body' => $body,
);
return $elements;
Use behat/automated tests.
<script>alert('XSS')</script>
<img src="a" onerror="alert('title')">
Check your filters and user roles.
Do not give too many options to untrusted
users!
Protection against Cross Site Scripting
Access Bypass
User can access/do something.
Menu items can be defined to be
accessed/denied.
Many access systems: node, entity, field, views...
Access bypass
Test II.
<?php
$query = db_select('node', 'n')
->fields('n', array('title', 'nid')
->condition('type', 'article');
$result = $query->execute();
?>
<?php
$query = db_select('node', 'n')
->fields('n', array('title', 'nid')
->condition('type', 'article');
$result = $query->execute();
?>
<?php
$query = db_select('node', 'n')
->fields('n', array('title', 'nid')
->condition('type', 'article')
->addTag('node_access');
$result = $query->execute();
?>
mymodule.not_found:
path: '/not-found'
defaults:
_controller: DrupalmymoduleControllerNotFoundController::build404
_title: 'Page not found'
requirements:
_access: 'TRUE'
mymodule.not_found:
path: '/not-found'
defaults:
_controller: DrupalmymoduleControllerNotFoundController::build404'
_title: 'Page not found'
requirements:
_access: 'TRUE'
Visit node/nid and other urls
Visit anything/%node
Use behat/automated tests.
node_access, entity_access
Menu definitions
user_access for permissions
$query->addTag('node_access')
Protection against Access bypass
SQL Injection
Unauthorized access to database resources.
Do not trust any user input.
SA-CORE-2014-005 – Highly critical D7 SA
SQL Injection
Test III.
<?php
$table = 'field_data_' . $field;
$sql = 'SELECT entity_id, bundle, ' . $field . '_linklabel FROM
{' . $table . '} WHERE ' . $field . '_normalized = :phoneno’;
$eid = db_query($sql, array(':phoneno' => $normalized))
->fetchAssoc();
?>
<?php
$table = 'field_data_' . $field;
$sql = 'SELECT entity_id, bundle, ' . $field . '_linklabel FROM
{' . $table . '} WHERE ' . $field . '_normalized = :phoneno’;
$eid = db_query($sql, array(':phoneno' => $normalized))
->fetchAssoc();
?>
<?php
$query = db_select('field_data_' . $field, 'fdf');
$query->fields('fdf', array('entity_id', 'bundle', $field .
'_linklabel'));
$query->condition('fdf.' . $field . '_normalized',
$normalized);
$eid = $query->execute()->fetchAssoc();
?>
Use always drupal Database API!
db_query with :placeholder (deprecated in D8,
in D9 will be removed)
Filter parameters
Check the queries in code.
username' AND 1=1
POST requests by curl
Protection against SQL Injection
Test IV.
Ready for some other code?
<?php
function _generate_password($length = 8) {
$pass = ’’;
for ($i = 0; $i < $length; $i++) {
// Each iteration, pick a random character from the
// allowable string and append it to the password:
$pass .= $allowable_characters[mt_rand(0, $len)];
}
}
?>
<?php
function _generate_password($length = 8) {
$pass = ’’;
for ($i = 0; $i < $length; $i++) {
// Each iteration, pick a random character from the
// allowable string and append it to the password:
$pass .= $allowable_characters[mt_rand(0, $len)];
}
}
?>
<?php
function _generate_password($length = 8) {
$pass = ’’;
for ($i = 0; $i < $length; $i++) {
do {
// Find a secure random number within the range needed.
$index = ord(drupal_random_bytes(1));
} while ($index > $len);
$pass .= $allowable_characters[$index];
}
}
?>
// custom_module.permissions.yml
administer custom module:
title: 'Bypass access control'
description: 'Allows a user to bypass access control.’
// custom_module.routing.yml
custom_module.settings.form:
path: '/admin/config/custom/settings'
requirements:
_permission: 'administer custom module'
// custom_module.permissions.yml
administer custom module:
title: 'Bypass access control'
description: 'Allows a user to bypass access control.’
// custom_module.routing.yml
custom_module.settings.form:
path: '/admin/config/custom/settings'
requirements:
_permission: 'administer custom module'
// custom_module.permissions.yml
administer custom module:
title: 'Bypass access control'
description: 'Allows a user to bypass access control.’
restrict access: TRUE
// custom_module.routing.yml
custom_module.settings.form:
path: '/admin/config/custom/settings'
requirements:
_permission: 'administer custom module'
// custom_module.routing.yml
custom_module.settings.form:
path: '/admin/config/custom/settings'
requirements:
_permission: 'administer site configuration'
// custom_module.routing.yml
custom_module.settings.form:
path: '/admin/config/custom/settings'
requirements:
_permission: 'administer site configuration'
Security Improvements
*https://events.drupal.org/sites/default/files/slides/pwolanin-2017-09-ways-drupal8-d.pdf
Many ways Drupal 8 is more secure!*
Twig templates for HTML generation
Removed PHP format
Site configuration exportable, versionable
User content entry and filtering improvements
User session and sessio always n ID handling
Automated CSRF token protection
Trusted host patterns enforced for requests
Single statement execution for SQL
Clickjacking protection
Content security policy compatibility with Core Javascript API
Learn by Advisories
Security advisories are for
 Only stable modules
 No alpha, beta, dev
 d.org hosted projects
@Maintainers: If you are contacted, be supportive! 
Drupal Security Team
Hacked!
Security review (simplytest.me)
Password policy
Encrypt
Composer Security Checker
Permission report
Text format reported
Drop Guard
+ PHPCS Drupal BestPractice Sniff
Security related projects
SecOSday – Haarlem edition
11 May, 2019
Questions?
Tatar Balazs Janos
@tatarbj
Thank you!

Let's write secure Drupal code! - DrupalCamp London 2019

Editor's Notes

  • #11 Einstein said: “insanity is when you do the same thing over and over again and expect different results”
  • #13 Owasp: open web application security project
  • #23 Hide enabled blocks from selector that are used Context update from this wednesday
  • #24 Hide enabled blocks from selector that are used Context update from this wednesday
  • #25 Hide enabled blocks from selector that are used Context update from this wednesday
  • #48 Not because of having db_query duplicated, but: The $field param is used to derive various table and field names, but in each case the Database API automatically escapes these values. Note that the API does not do this for all arguments!
  • #53 Mt_rand is not secure enough!
  • #54 Insecure randomness by Mass Password Reset (SA-CONTRIB-2018-043) by Greg Knaddison