How to write insecure Drupal 8 code

Hi, I'm Sam

dgo.to/@samuel.mortenson

@DrupalSAM

Yell at me later for....

  • SA-CORE-2015-003
  • SA-CORE-2015-004
  • SA-2017-001
  • SA-CORE-2017-002
  • SA-CORE-2017-003
  • SA-CORE-2019-003

Drupal is secure out of the box

*You can still mess stuff up

*

So you want to render HTML...

Are you rendering user input?

You may be vulnerable to cross site scripting.

(XSS)

1. Find user input

<script>
    document.write('<math><!--');
 </script>
 <i name="--><head><script>//">
alert(1)
<!--</script>-->

2. Craft clever exploit

3. Execute code

4. Celebrate!

Twig has your back

Every variable rendered normally is automatically filtered 👍

Attributes are also safe, in general

<b {{ attributes.addClass('"onmouseover="alert(1)"') }}>Hello</b>
> <b class="&quot;onload=&quot;alert&quot;">Hello</b>

How to mess it all up

Not wrapping attributes with double quotes

<b class={{ configurable_class }}>Hello</b>
$variables['configurable_class'] = 'foo onclick=alert(bar)';
> <b class=foo onclick=alert(bar)>Hello</b>

Variables are escaped with the XSS filter, but aren't aware of the context they're used in.

Attribute objects escape the ", &, ', <, and > characters, which is pretty darn safe.

Misusing render arrays

$build = ['#type' => 'html_tag', '#tag' => 'a', 'Hello'];
$build['#attributes']['href'] = $user_input;
$build['#markup'] = $user_input;
$build['#allowed_tags'] = ['script'];
$build['#children'] = $user_input;
$user_input = 'javascript:alert("foo")';
> <a href="javascript:alert(\"foo\")">Hello</a>
$build['#markup'] = t($user_input)

All of these allow <script>...</script>

$build = ['#type' => 'inline_template', '#template' => $user_input];

Not filtering in Javascript

var value = $('input.title').val();
$('.error').html('<p>Invalid title "' + value + '"</p>');
var value = $('input.title').val();
$('.error').text('Invalid title "' + value + '"');
var value = $('input.title').val();
$('.error').html(Drupal.t('<p>Invalid title "@title"</p>', {'@title': value});

Better

Best

General tips

Question complexity

Use Twig wherever possible

If you really need to manually escape user input, use standard methods:

  • check_markup($text, $format_id)
  • \Drupal\Component\Utility\Xss::filter($text)
  • \Drupal\Component\Utility\Html::escape($text)
  • Drupal.t(text)
  • Drupal.checkPlain(text)

So you want to query the database...

Are you querying based on user input?

You may be vulnerable to SQL injection.

(SQLi)

1. Find vulnerable query

"; DROP TABLE users;

" OR 1=1;

2. Craft clever exploit

3. Execute query, leading to data loss, information disclosure, access bypass, etc.

4. Celebrate!

Just use the abstractions

$uuid = \Drupal::database()
  ->select('node')
  ->fields('node', ['uuid'])
  ->condition('nid', $nid)
  ->execute()
  ->fetchField();
SELECT node.uuid AS uuid
FROM
node node
WHERE nid = :db_condition_placeholder_0
$nids = \Drupal::entityQuery('node')
  ->condition('title', $title)
  ->execute();
SELECT base_table.vid AS vid, base_table.nid AS nid
FROM
node base_table
INNER JOIN node_field_data node_field_data ON node_field_data.nid = base_table.nid
WHERE node_field_data.title LIKE :db_condition_placeholder_0 ESCAPE '\\'

Sidebar: SQL is crazy

node_load(1);
SELECT revision.vid AS vid, revision.langcode AS langcode, revision.revision_timestamp AS revision_timestamp, revision.revision_uid AS revision_uid, revision.revision_log AS revision_log, base.nid AS nid, base.type AS type, base.uuid AS uuid, CASE base.vid WHEN revision.vid THEN 1 ELSE 0 END AS isDefaultRevision
FROM
node base
INNER JOIN node_revision revision ON revision.vid = base.vid
WHERE base.nid IN (:db_condition_placeholder_0)SELECT revision.*
FROM
node_field_revision revision
WHERE (revision.nid IN (:db_condition_placeholder_0)) AND (revision.vid IN (:db_condition_placeholder_1))
ORDER BY revision.nid ASCSELECT t.*
FROM
node__body t
WHERE (entity_id IN (:db_condition_placeholder_0)) AND (deleted = :db_condition_placeholder_1) AND (langcode IN (:db_condition_placeholder_2, :db_condition_placeholder_3, :db_condition_placeholder_4, :db_condition_placeholder_5, :db_condition_placeholder_6))
ORDER BY delta ASCSELECT t.*
FROM
node__field_meta_tags t
WHERE (entity_id IN (:db_condition_placeholder_0)) AND (deleted = :db_condition_placeholder_1) AND (langcode IN (:db_condition_placeholder_2, :db_condition_placeholder_3, :db_condition_placeholder_4, :db_condition_placeholder_5, :db_condition_placeholder_6))
ORDER BY delta ASCSELECT t.*
FROM
node__panelizer t
WHERE (entity_id IN (:db_condition_placeholder_0)) AND (deleted = :db_condition_placeholder_1) AND (langcode IN (:db_condition_placeholder_2, :db_condition_placeholder_3, :db_condition_placeholder_4, :db_condition_placeholder_5, :db_condition_placeholder_6))
ORDER BY delta ASCSELECT ces.*
FROM
comment_entity_statistics ces
WHERE (ces.entity_id IN (:db_condition_placeholder_0)) AND (ces.entity_type = :db_condition_placeholder_1)⏎

How to mess it all up

Not using placeholders

\Drupal::database()
  ->query("DELETE FROM people WHERE name = \"$name\"")
  ->execute();
\Drupal::database()
  ->query('DELETE FROM people WHERE name = :name', [
    ':name' => $name,
  ])
  ->execute();
$name = 'myname" OR "" = "';
> DELETE FROM people WHERE name = "myname" OR "" = ""

Using placeholders

Not escaping LIKE

$result = \Drupal::database()
  ->delete('people')
  ->condition('name', '%_' . $name, 'LIKE')
  ->execute();
$name = '%';
> DELETE FROM people WHERE name LIKE "%_%"
$database = \Drupal::database();
$result = $database
  ->delete('people')
  ->condition('name', '%_' . $database->escapeLike($name), 'LIKE')
  ->execute();

Escaping like

General tips

Use placeholders everywhere

Try not to write queries by hand

Escape conditions when using LIKE

So you want to write some code...

Are you executing based on user input?

You may be vulnerable to Remote Code Execution.

(RCE)

1. Find user input

POST '/provision-environment/dev";\
echo "'.$key.'" >> ~/.ssh/authorized_keys'

2. Craft clever exploit

3. Execute code

4. Celebrate!

$ ./provision-env.sh "dev";\
echo "ssh-rsa ..." >> ~/.ssh/authorized_keys

No golden rule

But you should watch out for the classics

`magick convert $filename output.png`;
shell_exec("magick convert $filename output.png");
preg_replace($user_regex, $user_replacement, $user_subject);
preg_replace('/.*/e', 'system("whoami")', "hacked");

( PHP < 7 )

unserialize($user_input);
eval("is_null($user_input)");
shell_exec('magick convert ' . escapeshellarg($filename) . ' output.png');
preg_replace('/' . preg_quote($user_regex) . '/i', $user_replacement, $user_subject);

RCE can be hard to spot!

{{ content }}
{{ {'#markup': '<b>Render arrays are cool!</b>'} }}
{{ {"#lazy_builder": ["shell_exec", ["touch /tmp/hello"]]} }}

https://www.drupal.org/node/2860607

😱

😱

General tips

Watch out for dynamic shell commands

Never, ever, ever unserialize user input

Try not to use eval or any equivalent

The future of Drupal exploits is headless

Headless Drupal relies mostly on entity validation

Form validation

User input

Route access

Entity validation

User input

Entity validation

Traditional Drupal

Headless Drupal with REST

Resource access

Serializer validation

Headless implementations are not mature

How do you render formatted text in a headless app?

How much validation should an app do before sending data to Drupal?

How should credentials be stored in a headless app?

A parting thought

Make security a part of your work, culture, and code.

Dcon seattle - How to write insecure Drupal 8 code

By mortenson

Dcon seattle - How to write insecure Drupal 8 code

A presentation about how you can still write insecure code in Drupal 8, even with all it does to stop you. 😁

  • 1,086