Upgrade Your Drupal Skills

We trained 1,000+ Drupal Developers over the last decade.

See Advanced Courses NAH, I know Enough
Jul 30 2013
Jul 30

I've recently seen some recommendations to use hook_hook_info to provide "groups" to core-provided hooks so you can move your module's implementations of those hooks to $module.$group.inc. A poor-mans autoloader if you will.

Beware that such use is not in accordance with the API documentation which states that hook_hook_info should be implemented by modules declaring new hooks for other modules to implement.

In addition, having two modules on your system declaring a group for the same hook will break all use of $group.inc's for that particular hook. This happens regardless of whether the group is identical or not.

To reiterate:
  • DON'T use if all you do is implement hooks.
  • DO use to declare own hooks that can be used in other modules.

NB: In rare cases, a module could implement hook_hook_info_alter to change a hook group, but this is again pretty hacky, for modules could still opt to have hook implementations in their main .module file.

Feb 18 2013
Feb 18

I recently evaluated the Bakery Single Sign-On System aka Bakery SSO aka Bakery on behalf of clients. This article describes how I moved from finding a small weakness in version 2.x-alpha-3 to an exploit.

If you haven't updated all your sites to Bakery 2.0-alpha4 (6.x, 7.x), go and do so now.

About Bakery

Bakery provides a "single sign on" feature for Drupal based sites that are on the same second-level domain (i.e. example.com, subsite.example.com, subsite2.example.com). The Drupal.org family of sites uses Bakery so you can login on Drupal.org then visit and browse groups.drupal.org, association.drupal.org or drupalconlatest.drupal.org as an authenticated user without having to login again.

Very handy indeed.

How Bakery works

Bakery knows two types of sites, the master and slaves. The Bakery master is the only site that actually handles user authentication via username and password. Slaves forward login data to the master, but do not authenticate the user themselves via username and password.

Bakery works by passing around data in what it calls "cookies", confusingly both in actual (shared) cookies and in POST requests. These cookies contain the passed data together with a signature (a HMAC) in a base64-encoded encrypted string. The signature should ensure message authenticity (ie actually originates from master or slave). The encryption and signing key is shared between master and slaves. While Bakery makes a number of cookies, the two most important for this entry in the bug2exploit series are Oatmeal and Chocolatechip.

If you login on slave S, it will forward the username and password to the Bakery master M in Oatmeal via a browser redirect. The Bakery master does some checks, logs in the user and creates a Chocolatechip cookie only to redirect the user back to the slave. The slave checks the Chocolatechip cookie, updates relevant information and logs the user in.

While Chocolatechip serves to authenticate a user to the slaves, it also works to authenticate to the master. The cookie carries the username, mail and init together with some housekeeping fields.

Encryption & signatures

Before we continue to the weakness in Bakery, we need to take a look at the structure of cookies. Whenever Bakery bakes a cookie it starts by building an array with the data to send and a HMAC-based signature.

The next step in baking a cookie is to serialize the array structure, encrypt it via AES in ECB mode using the shared private key and finally base64-encode the result.

When the Bakery master receives such a cookie, it runs the process in reverse. First, it base64-decodes the contents, decrypts the result, then unserializes this into a $cookie array. Bakery then calculates a signature and checks if it matches the signature delivered in the cookie array.

Weakness: Unserialize on cookie value

Any security researcher will perk up when she sees an unserialize on user-supplied data. Such a call can be used to invoke object destructors with values chosen by an attacker. On Drupal 7, this can be used to delete files from the filesystem. As signature verification happens after unserializing the data, it cannot interfere with our nefarious purposes.

Alas, as Bakery decrypts the cookie before it calls unserialize on it, we need to find a way to get our payload encrypted with the private key or the decryption step will result in random garbage. This is where the next weakness comes in.

Weakness: ECB mode

AES is a block cipher that can be used in a variety of modes. In Electronic cookbook mode (ECB) used by Bakery, the plaintext is split in 128-bit blocks and each block is separately encrypted with the key. The content of one block doesn't influence the ciphertext of the next block.

Here's a short example, with a reduced blocksize of 64-bits to illustrate the consequences. A string that fits into 2 64-bit blocks has been encrypted in ECB mode. We then take the ciphertext, split it in 64-bit blocks and swap their positions. When we decrypt, the corresponding plaintext blocks have also switched position.

h e i n e j a n  d e e l s t r

4e2387db4b9713f0 5e9275df5301bf8b

5e9275df5301bf8b 4e2387db4b9713f0

d e e l s t r  h e i n e j a

This property of ECB means that we can swap, shuffle and delete blocks or even insert them into other encrypted messages to get control over the decryption result. All we need to take care of is to align injected data to block borders and to identify these blocks in the ciphertext. Note that injected data may span multiple blocks.

So, in order to exploit the unserialize call, we need to have Bakery encrypt our payload and make sure this payload gets aligned to an AES block start. The best candidate is the Oatmeal cookie, as it contains data we supply via the login form.

Here's the PHP data-structure of Oatmeal after a slave login, just before it is sent to the encryption pipeline:

$cookie = array (
     // data comes from form_state['values']
    'data' => array (        
        'name' => 'username',
        'pass' => 'mypassword',
        'op' => 'Log in',
    'name' => 'username',
    'calories' => 320,
    'timestamp' => 1358887168,
    'master' => 0,
    'slave' => 'http://slave.yawning-angel/',
    'signature' => 'complicated_looking_signature_here',

Under special circumstances 'data' may contain additional fields. On most sites however, this is what we have to work with. Serialized and split in 128-bit blocks this looks like:






"op";s:6:"Log in

";} .......... }

Remember that these blocks correspond 1:1 with the blocks after encryption. Supplying an 8 character 'username' gets the password value aligned to the start of the fifth block. One can simply delete the first four blocks to get a decryptable cookie that starts with user-supplied data. Because unserialize doesn't care about trailing characters after valid input, there's no need to worry about the extra blocks on the end, provided the "password" itself can be unserialized.

With all that encryption out of the way, lets see if we can do something fun with the payload that works on both Drupal 6 and Drupal 7 sites. Can we bypass the signature check and forge valid Chocolatechip authentication cookies?

Yes we can.

Weakness: Type juggling

Here's a typical signature comparison executed by Bakery. The received signature is compared to a hash derived from several received fields and the Bakery secret key. The age of the cookie is determined as well.

// $signature is calculated as a hash_hmac over some cookie fields.
if ($signature == $cookie['signature'] &&
    $cookie['timestamp'] + variable_get('bakery_freshness', '3600') >= $_SERVER['REQUEST_TIME']) {
  $valid = TRUE;

The comparison operator == has some interesting type juggling properties. If you supply a boolean TRUE on one side and a non-empty string on the other (such as 'impressive hash' == TRUE), the expression evaluates to TRUE. Because an attacker can control the type of $cookie['signature'] via the unserializable payload the signature check is no obstacle. A serialized boolean true for signature would look like s:9:"signature";b:1;.


In order to forge a valid Chocolatechip for a targeted user account, the payload needs to meet the following requirements:

  • contain boolean signature
  • contain a valid timestamp
  • contain the username of the target
  • contain the email address of the target
  • all this serialized …
  • …but in 128 chars or less

Here's a payload to attack the user account 'deelstra' with [email protected] as mail address. The exponential notation for a future timestamp (2E9) allows additional room to spend on username and email.

a:4:{s:9:"signature";b:1;s:9:"timestamp";s:3:"2E9";s:4:"name";s:8:"deelstra";s:4:"mail";s:15:"[email protected]";}

Using the string above as the value for password gets us the following structure of Oatmeal. The actual Oatmeal cookie is of course encrypted, but will correspond to this block layout. Throw away the first 4 blocks and Oatmeal is transformed into a valid Chocolatechip cookie.













Log in";} .....}

While this specific exploit depends on knowing the email address of a targeted user, do not base your upgrade decision on this detail. Exploits that do not require any additional information beyond the user id are also possible.


Bakery 2.x-alpha4 has been modified to guard against these types of attacks. It still uses ECB (this will change in another release) but cookie handling has markedly improved:

  • Signature is sent outside of the encrypted data
  • Signature verification uses the === operator
  • Signature covers the entire encrypted cookie (block order, all fields)
  • Cookie type information has been added to the cookies and is checked on receipt
Oct 24 2012
Oct 24


SA-CORE-2012-003 fixes an issue in the Drupal installer that enables an attacker to cause the site to use a different attacker-controlled database. This database can be an external server or an SQLite file. The vulnerability also causes the installer to leak database information such as the database type, name, host and the username used to connect to the database. The only item not leaked is the database password.

The installer vulnerability was found while preparing my DrupalJam presentation (NL) on security audits and reported via the SecuriTeam Secure Disclosure program (also via [email protected]). As promised on IRC & Reddit, here's some additional information on the root cause(s).

Installer system

The vulnerability is caused by an assumption of the install system combined with a bug. The assumption is that errors generated while contacting the database indicate a system that still needs installation. To aid in understanding the additional bug, some global information on the installer follows first.

The problem revolves around the function install_begin_request. It verifies the validity of the current settings.php via install_verify_settings, then determines what the install state is in order to decide if installation should continue or if the site has already been installed.

Here’s the function (elided for brevity):

function install_begin_request(&$install_state) {
  // ...
  $install_state['settings_verified'] = install_verify_settings();

  if ($install_state['settings_verified']) {
    // Initialize the database system. Note that the connection
    // won't be initialized until it is actually requested.
    require_once DRUPAL_ROOT . '/includes/database/database.inc';

    // Verify the last completed task in the database, if there is one.
    $task = install_verify_completed_task();
  else {
    $task = NULL;

    // Since previous versions of Drupal stored database connection information
    // in the 'db_url' variable, we should never let an installation proceed if
    // this variable is defined and the settings file was not verified above
    // (otherwise we risk installing over an existing site whose settings file
    // has not yet been updated).
    if (!empty($GLOBALS['db_url'])) {
      throw new Exception(install_already_done_error());

  // Modify the installation state as appropriate.
  $install_state['completed_task'] = $task;
  $install_state['database_tables_exist'] = !empty($task);

When install_verify_settings does not succeed in verifying settings.php the else path is taken, and installation will proceed, provided there's no global db_url set (this global indicates a D6 settings.php and marks a system that is to be updated).

install_verify_settings will ultimately run db_run_tasks (a wrapper for the DatabaseTasks->runTasks method). The tasks executed in db_run_tasks are listed in install.inc in the abstract class DatabaseTasks and consist of table creation, insertion, deletions and dropping.

Any exceptions generated during db_run_tasks will be reported as errors to install_verify_settings. This means that when errors of any kind (eg database is down) are generated by the db, the settings.php file fails verification and installation will proceed by displaying a partially filled in Database configuration page.

If required information is send in a POST request, the installer will mark settings.php as writable (if possible) and then update settings.php with this information.


Being able to do a new install when the database is down, or settings.php is temporarily broken is bad enough, but not what makes SA-CORE-2012-003 so grave. The big problem is the possibility to force the verifier to error out on a working database.

To see how, let's look at the database tasks install_verify_settings runs:

  • CREATE TABLE {drupal_install_test} (id int NULL)
  • DROP TABLE {drupal_install_test}

You probably see the problem now. If not, consider two POST requests fired nearly simultaneously at install.php:

  • Request 1 executes the db-task CREATE TABLE {drupal_install_test} (id int NULL); The table will be created.
  • Request 2 executes the db-task CREATE TABLE {drupal_install_test} (id int NULL); Oops…
  • Request 1 executes the rest of the db-tasks and then deletes drupal_install_test

Trying to create an already existing table in Request 2 will generate an exception that will be reported as a db/settings error to install_begin_request and thus a signal for a continued installation. By sending the right information in the POST request, a settings.php is written that points to the attacker controlled database.


I'm comfortable releasing this information after a week, because the mitigation step is pretty easy and without consequence: delete install.php or block access to it. If you didn't upgrade or follow the mitigation steps, do so now, then review your update procedures. I conciously did not provide a weaponized example of the exploit; While it's easy to create such an exploit based on this article, it is a step many attackers cannot or will not take.

Should you learn of a serious vulnerability in Drupal or another piece of software, consider the SecuriTeam Secure Disclosure program (also via [email protected]). I had a great experience.

Jun 11 2012
Jun 11

As a Drupal developer (or in broader terms: as someone who administers and/or is responsible for (a) Drupal site(s)), if you have found a vulnerability, you must assume that someone else may find that vulnerability as well. So the only way to ensure that your sites are not being exploited themselves is
A: Fix the vulnerability yourself
B: Report the vulnerability to the Security team
In general, the Security team will have more expertise in this area than you have yourself, so B is really the only sensible thing to do.

As a hacker, though, who does not have any affiliation with Drupal specifically, this incentive does not apply. These are the people we can (and, in my opinion, should) animate to do the right thing with a bounty.

Mar 09 2012
Mar 09

Update: The Drupal security team just published an official, detailed response.

On March 2nd 2012, security researcher Ivano Binetti published an advisory on Drupal 7 anti-CSRF measures. He/She rightly identified the long standing Logout CSRF annoyance (#144538), but the rest of his/her advisory is not helpful.

Form Build ID

Contrary to what's said in the advisory, Drupal 6 and 7 do not use the form-build-ID to protect against CSRF. The build-ID is used to fetch state from a database table during certain operations.

Anti-CSRF token system

In accordance with OWASP CSRF recommendations Drupal uses the field form_token to protect against CSRF with a challenge token that is tied to the current user's session and the form's form_id (not form-build-ID). The form_token has to remain secret.

Defeat the anti-CSRF token system

There are several ways for the anti-CSRF challenge token system to be defeated:

  • When the site has a Cross site scripting (XSS) vulnerability.
  • When traffic between an authorized user agent and the server can be sniffed.
  • When traffic between an authorized user agent and the server can be intercepted, changed and send on in a Man In The Middle attack (MITM).

The token challenge system was not designed to cope with these issues as the defenses were either pointless or impossible. As an example; When traffic can be sniffed or intercepted, an attacker can in typical cases simply extract the authorized user's password or session ID and use these to get access to the site.

The proposed HTTP_REFERRER check in 2.4 is also not very helpful. It fails just like the challenge token system when the site has an XSS vulnerability. In addition, user-generated content on the same site can also bypass the check by design.


In order to prevent a bypass of the anti-CSRF system, Drupal developers or site administrators should use appropriate defenses:

  • Prevent sniffing and MITM with HTTPS.
  • Prevent XSS by using the appropriate APIs.


Apart from the Logout link, the advisory does not identify an exploitable CSRF issue in Drupal 7.

This is not an official security team response. I've stepped down as team lead.

Mar 29 2011
Mar 29
In CSS, identifiers (including element names, classes, and IDs in selectors) can contain only the characters [a-zA-Z0-9] and ISO 10646 characters U+00A0 and higher, plus the hyphen (-) and the underscore (_); they cannot start with a digit, two hyphens, or a hyphen followed by a digit.

Identifiers can also contain escaped characters and any ISO 10646 character as a numeric code (see next item). For instance, the identifier "B&W?" may be written as "B\&W\?" or "B\26 W\3F".

Jan 10 2011
Jan 10

Today we released a security announcement about a Webform SQL Injection vulnerability outside of the normal release schedule on Wednesday.

I chose to release today with a minimal fix instead of waiting until January 12th for a combination of reasons:

  1. The vulnerability was made public.
  2. The injection requires no permissions at all.
  3. High impact; easy uid 1 access.
  4. No other user interaction required.
  5. Webform was under high scrutiny last week due to the Geenstijl shockblog.
  6. We received news today that the hole was being actively exploited.

This combination could turn out to be very damaging for a lot of Drupal sites should we have waited longer.


To clear up any confusion regarding the affected supported branches; only Webform 6.x-3.x is affected. Users of Webform 6.x.3.x should upgrade to Webform 6.x-3.5.

The Webform 6.x-2.x versions are not affected by this vulnerability. As long as you use 6.x-2.8, 6.x-2.9 or 6.x-2.10 you're good. Older versions of the Webform 6.x-2.x branch have different vulnerabilities that were already announced.

Webform for Drupal 5.x and the 7.x betas are not supported by the security team.

Aug 25 2010
Aug 25

Apart from PHP bugs and Denial of Service attacks, there's another reason why calling unserialize on user-supplied data (cookies, hidden form fields) is a bad idea.

When a serialized object is unserialized, its __wakeup() member function will be called if it exists. Lesser known is the fact that another magic method will also be invoked. This is the __destruct() member function that will be invoked when either 1) all references to the object are removed, 2) when the object is explicitly destroyed or 3) when the script ends.

You can see for yourself with the following example code:

class someClass {
  public $foo;

  public function __destruct() {
    echo "__destruct(): Exterminate! Exterminate!\n";

echo "Back in main.\n";

function demo() {
  echo "Demo starts.\n";
  $s = serialize(new SomeClass());

  $u = unserialize($s);
  echo "Demo's over.\n";

This will output:

Demo starts.
__destruct(): Exterminate! Exterminate!        <-- no refs to the new result, object destroyed.
string(32) "O:9:"someClass":1:{s:3:"foo";N;}"
object(someClass)#1 (1) {
Demo's over.
__destruct(): Exterminate! Exterminate!        <-- $u out of scope, object has no refs, destroyed.
Back in main.

The first destruct happens when the result of new SomeClass() is destroyed, the second one when the $u variable goes out of scope and the last reference to the object is removed.

As all the member variables can be controlled by the person supplying the string, he (m/f/o) can cause a __destruct() member function to operate on unexpected data. Stefan Esser has used this to great effect; See Shocking news in PHP exploitation and the Piwik cookie unserialize vulnerability for how he was able to exploit this to run arbitrary code.

Drupal 7 also contains a __destruct() that, combined with the autoloader, can be used to delete arbitrary files from the system, provided the server running PHP has sufficient privileges to do so.

The __destruct() in question is a member function of Archive_Tar:

function __destruct()

function _close() {
  if ($this->_temp_tarname != '') {

So, if a module uses unserialize on user-supplied data, one can simply provide the following string (adapting _temp_tarname) to do damage:

O:11:"Archive_Tar":6:{s:8:"_tarname";N;s:9:"_compress";b:0;s:14:"_compress_type";s:4:"none";s:10:"_separator";s:1:" ";s:5:"_file";i:0;s:13:"_temp_tarname";s:0:"";}

Take away message: do not call unserialize on user-supplied data.

Apr 28 2010
Apr 28

We recently received a report by "ZeroDayScan", about a "Full path disclosure bug in Drupal 6.16".

You can read the story @ http://blog.zerodayscan.com/2010/04/full-path-disclosure-bug-in-drupal-6.... As my short comment was removed from the post, I have to resort to a blogpost. My apologies for polluting the Planet.

Summary of the issue: If you set error reporting to the default value "Write errors to the log and to the screen", the installation path is displayed on the ...*drumroll*... screen.

Which is of course the point.

Calling the setting a "workaround", the default a "bug" and a "vulnerability" is either idiocy, or insincere. Now that comments were removed, we know. Insincere and at the same time a great way to highlight the impotence of the ZeroDayScan scanner.

My last message to ZeroDayScan: If there's an SQL injection on a Drupal site; you can simply take over the site as uid 1 (root); no need to find out the full path via an obscure error message.

Jan 13 2010
Jan 13

Is your recent Drupal update not taking effect? Drupal still claims to be the old version?

It is probably correct! There's at least one module on your system that claims it is 1) a core module and 2) old. How did this happen? Common scenarios are:

  • You accidentally restored the old modules folder from a backup.
  • You tried to overwrite the older install, but this failed for some reason (common on ftp).
  • You made a backup of core modules inside the modules or sites folder.
  • You copied a core module to sites/default/modules or sites/[site]/modules to override a core module.
  • You are looking at the wrong server (embarrassing, but it happens).

Remember, Drupal prefers core files in sites/all/modules or sites/[site]/modules over those in modules when it finds copies.

To identify the actual files in use, check the filename and info columns for core modules in the system table. If you don't like touching your database, install the Update: DOH! module. It will give you a list of filenames and their versions.

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web