HMAC best practices
This documentation needs review. See "Help improve this page" in the sidebar.
Summary
This article aims to describe best practices that protect against certain classes of vulnerabilities when operating on messages signed with Hash based Message Authentication Codes (HMACs).
HMACs guarantee that a particular message was produced by an agent that possesses the shared key. They are often used within Drupal to authenticate messages between subsystems within the same site.
To generate HMACs intended for use within a site, use \Drupal\Component\Utility\Crypt::hmacBase64($data, $key) with the following parameters:
- $data: "{$issuer}:{$message_type}:{$created}:{field_1}:{$field_2}:{$field_3}:..."
- $key: \Drupal::service('private_key')->get() . \Drupal\Core\Site\Settings::get('hash_salt')
Issuer, typically the issuing module, and message type serve to differentiate between the various message types that may be used in the site.
To validate a message with an HMAC, reconstruct the message, and generate a validation HMAC using the same key. Compare the generated HMAC and received HMAC using hash_equals($generated_hmac, $received_hmac). Next, ensure the HMAC is a non-empty string. When the HMAC is correct, validate the message itself by checking the issuer, message type and created timestamp.
Never sign messages that are entirely user provided.
Security properties guaranteed by a validated HMAC
A valid HMAC of a message with an adequate key (see below) guarantees that the message was produced by an agent possessing the shared secret key. It does not guarantee that the message was intentionally signed, nor does it guarantee the purpose of the message, unless additional steps are taken.
For example; an HMAC over a user id (uid) is indistinguishable from an HMAC over a file id (fid) if uid == fid. Messages should always be namespaced, ideally by issuer and message type.
HMAC("$uid", $key) === HMAC("$fid", $key); // for $uid == $fid
HMAC("user:ticket:{$uid}", $key) !== HMAC("file:download:{$fid}", $key); // for $uid == $fidMessage construction
Always namespace messages by issuer, and where multiple different messages may be sent, a message type. Add a creation timestamp to allow HMACs to expire. Note that expiry needs to be implemented during HMAC and message validation (see below). When generating a message containing multiple fields, ensure the fields are separated from each other by an arbitrary field separator (eg ':').
Field separators are used to prevent identical HMACs by shifting bytes between fields. As an example; The output of HMAC("user:ticket:{$uid}{$mail}", $key) is identical for the combination $uid = '100' and $mail = 'foo@example.com' and the combination $uid = '10' and $mail = '0foo@example.com'. This particular weakness has led to significant breaches in the past.
An HMAC using field separators, HMAC("user:ticket:{$uid}:{$mail}", $key), cannot be abused in this manner.
HMAC and message validation
Reconstruct the message, and generate a validation HMAC using the same key. Compare the generated HMAC and received HMAC using hash_equals($generated_hmac, $received_hmac). The identical (===) and equals (==) operator and their negated variants have undesirable properties for hash validation. Do NOT use them to verify HMACs.
Verify separately that the HMACs are non-empty strings.
The above validates that the HMAC is valid, not whether the controlling function should further process the message. Validate the message further by verifying the issuer and message type against an allowed list. When a message should have a limited lifetime, validate that the timestamp is not expired. Also validate that the message has not been created with a timestamp in the future.
Key usage
The key parameter should always be a string that is at least 32 bytes. The key should be randomly generated with a cryptographically safe random generator such as random_bytes(32) and stored for further use.
Messages for internal use should usually be signed with a concatenation of the drupal private key and the hash salt available from Drupal settings. The use of the hash salt prevents malicious users from exploiting read-only SQL-injections or misplaced database backups. It also prevents HMACs from being generated on one environment, such as a test environment, and being used on another environment such as a production environment. This requires setting environment specific hash salts in their respective settings.php file. HMACs intended for use where a database is not available should use the hash_salt as the key.
Messages intended for external use cannot be signed with the Drupal private key or with the hash salt, because sharing those keys would allow the external party to sign messages intended for internal use.
In case of external HMACs, generate a party-specific random key and store it for future use. To decrease the attack surface against database access, either via readonly SQL-injection or a leaked database backup, you may want to take additional steps.
One could for example generate a party-specific random key and store this in the database, but use hash_hmac('sha256', $key, $hash_salt) as the shared key between parties.
User provided messages
Never sign messages that are entirely user provided. Doing so allows users to forge arbitrary messages representing internal system messages, using the same key.
// DON’T
HMAC("$user_provided_string", $key);
// DO
HMAC("{$issuer}:{$type}:{...}:{$user_provided_string}", $key);Help improve this page
You can:
- Log in, click Edit, and edit this page
- Log in, click Discuss, update the Page status value, and suggest an improvement
- Log in and create a Documentation issue with your suggestion
Still on Drupal 7? Security support for Drupal 7 ended on 5 January 2025. Please visit our Drupal 7 End of Life resources page to review all of your options.