Token or secret column stored in plain text
- Rule:
schema-plaintext-secret-column - Default severity: Medium
- CWE: CWE-312 (Cleartext Storage of Sensitive Information)
- Risk category: Data protection
A token, API key or secret column is written with the raw value. Anyone who can read the table (a leaked backup, a read-only SQL injection, an over-broad analytics replica) can replay the token or key directly.
What Vulkro detects
Vulkro finds the column in the database schema and every place the code writes it. The rule fires when none of those write sites hashes or encrypts the value. Columns that only look like secrets, such as a key's last four digits or a public prefix, are not counted.
Remediation. If the application only needs to check the value later (session, refresh, reset, verification or API tokens it issued itself), store a SHA-256 (or HMAC) digest and compare digests: the raw value is shown to the user once and never stored. If the application must use the value later (a third-party API key or OAuth token), encrypt it at rest with a key kept outside the database (a KMS, a Vault transit key, or an application-level envelope key). Rotate the existing values after the change.
Non-compliant code (example)
// The raw reset token is stored, so a database read replays it.
exports.forgotPassword = async (req, res) => {
const token = crypto.randomBytes(32).toString('hex');
await User.updateOne(
{ email: req.body.email },
{ resetToken: token, resetTokenExpiresAt: Date.now() + 3600000 }
);
await sendResetMail(req.body.email, token);
res.sendStatus(204);
};
Compliant code (example)
// Only a SHA-256 digest of the reset token is stored.
exports.forgotPassword = async (req, res) => {
const token = crypto.randomBytes(32).toString('hex');
const tokenDigest = crypto.createHash('sha256').update(token).digest('hex');
await User.updateOne(
{ email: req.body.email },
{ resetToken: tokenDigest, resetTokenExpiresAt: Date.now() + 3600000 }
);
await sendResetMail(req.body.email, token);
res.sendStatus(204);
};