Password Peppering
debt(d7/e5/b5/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints indicate semgrep can flag absence of pepper patterns, but the automated field is 'no' — semgrep rules require manual configuration and pattern definition for something as conceptually nuanced as 'password_hash without pepper'. The real failure (e.g. pepper stored in DB, hardcoded in source) requires careful code review to spot. No standard linter catches this by default.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix describes prepending a pepper from env vars, which sounds simple, but common_mistakes include pepper rotation (requires re-hashing all passwords at next login) and correcting misuse (e.g. moving a pepper from DB to env, fixing HMAC ordering). This touches authentication flows, environment configuration, and potentially deployment pipelines — more than a single-line swap but not a full architectural rework.
Closest to 'persistent productivity tax' (b5). Peppering applies to the web authentication context broadly and imposes ongoing operational concerns: pepper must be stored securely outside the DB, rotated carefully, and every developer touching auth must understand the pattern. It shapes how password hashing is done across the codebase but does not define the entire system's architecture — closer to b5 than b7.
Closest to 'serious trap (contradicts how a similar concept works elsewhere)' (t7). The misconception field explicitly identifies the core trap: developers assume a pepper in config is functionally equivalent to a salt in the DB, misunderstanding that the pepper's value comes from being stored *outside* the database. Common mistakes reinforce this — storing pepper in the DB, hardcoding it in version-controlled source, or misapplying HMAC ordering. These are plausible mistakes a competent developer would make without prior exposure to peppering.
Also Known As
TL;DR
Explanation
A pepper is a secret value (distinct from a salt) that is the same for all users, stored outside the database (environment variable, secrets manager, HSM) and concatenated with the password before hashing. If an attacker steals the database, they have hashes and salts but not the pepper — offline cracking is impossible without it. In PHP, prepend or HMAC the pepper before password_hash(): $peppered = hash_hmac('sha256', $password, $pepper); password_hash($peppered, PASSWORD_ARGON2ID). Rotate peppers by re-hashing on next login.
Watch Out
Common Misconception
Why It Matters
Common Mistakes
- Storing the pepper in the database alongside the hashes — defeats the entire purpose.
- Using a hardcoded pepper in source code that is committed to version control.
- Using HMAC-then-hash rather than hash(password + pepper) — the order and method matters for security properties.
- Not planning for pepper rotation — changing the pepper requires re-hashing all passwords at next login.
Code Examples
// Pepper stored in the same DB as hashes:
$pepper = $db->query('SELECT value FROM config WHERE key = "pepper"');
$hash = password_hash($password . $pepper, PASSWORD_BCRYPT);
// Database breach reveals both the hash and the pepper
// Pepper is an application-level secret added before hashing
// Unlike salt (stored in DB), pepper is in environment config
// A compromised DB dump is useless without the pepper
$pepper = $_ENV['PASSWORD_PEPPER']; // e.g. 64-char random string
$peppered = hash_hmac('sha256', $plaintext, $pepper);
$hash = password_hash($peppered, PASSWORD_ARGON2ID);
// Verify
$peppered = hash_hmac('sha256', $input, $pepper);
if (password_verify($peppered, $storedHash)) { /* authenticated */ }
// Rotate pepper: add new pepper env var, re-hash on next successful login