Skip to content

Crypt-Password uses DES crypt with a hard-coded salt, truncating stored passwords to 8 characters

Moderate
lirantal published GHSA-8fhv-vgrw-j9rv Aug 13, 2026

Package

No package listed

Affected versions

master

Patched versions

None

Description

Summary

Crypt-Password values written by daloRADIUS are hashed with traditional DES crypt(3)
using a single hard-coded salt. This has three consequences:

  1. Passwords are silently truncated to 8 characters. A 20-character password is stored
    as the hash of its first 8 characters, and authenticates with only those 8 characters.
  2. Every user shares the same salt (SA), so one precomputed table breaks all accounts
    at once, and identical passwords are visible as identical hashes.
  3. DES crypt(3) is computationally trivial to brute force on modern hardware.

The affected values live in the RADIUS radcheck table, i.e. the credentials that
authenticate users to the networks daloRADIUS manages.

Operators who deliberately choose Crypt-Password over Cleartext-Password are choosing
the option that looks safest in the UI, so the weakness is not visible to them.

Status: still present on current master

Verified against raw.githubusercontent.com/lirantal/daloradius/master at the time of
writing — the Crypt-Password case is unchanged.

This is worth flagging because the function was edited recently: "feat: add SHA2 password
support (#716)" (2026-06-22)
added a SHA2-Password case immediately below the affected
line, so the surrounding code has had attention without the DES salt being noticed. No
published advisory covers it either; the only advisory on the repository is
GHSA-c9xx-6mvw-9v84 (XSS + CSRF, 2022-12-06). I could not find prior public discussion,
so I do not believe this is a duplicate — but as your policy keeps reports private until a
fix ships, please disregard this if it has already been reported through that channel.

Affected code

Two copies of the same function, both on master
(observed at commit 9b5b07847d28345270bba797e960cfec2fab3fce, 2026-08-05):

File Used for
app/operators/library/attributes.phphashPasswordAttribute() hashing on create/update
app/users/pref-auth-password-edit.phphashPasswordAttribute() hashing and verification
case "Crypt-Password":
    return crypt($value, 'SALT_DALORADIUS');

PHP's crypt() selects the algorithm from the salt prefix. Because SALT_DALORADIUS
does not begin with $, it is interpreted as a traditional DES salt, and only its first
two characters (SA) are used.

Reached from at least:

  • app/operators/mng-new.php (via attributes.php:234)
  • app/operators/mng-batch-add.php:287,295
  • app/operators/mng-import-users.php:227
  • app/users/pref-auth-password-edit.php:127,132

Reproduction

<?php
$a = crypt("CorrectHorseBatteryStaple", 'SALT_DALORADIUS');
$b = crypt("CorrectH",                  'SALT_DALORADIUS');   // first 8 characters
var_dump($a === $b);                    // bool(true)
var_dump($a);                           // string(13) "SA..."
var_dump(substr(crypt("anything", 'SALT_DALORADIUS'), 0, 2)); // "SA" for every user

Observed on PHP 8.1.2 (Ubuntu 22.04, aarch64). Verified end to end by creating a user
through Management → Users → New User with a 16-character password and
Crypt-Password, then authenticating successfully against FreeRADIUS 3.0.26 (PAP) using
only the first 8 characters.

Impact

An attacker who obtains a copy of radcheck — a database backup, a SQL injection, a
compromised host — recovers the stored passwords with very little work. The 8-character
truncation caps the search space regardless of the password the operator chose, and the
shared salt lets a single table be reused across every account and every daloRADIUS
installation.

Suggested classification: CWE-916 (insufficient computational effort) and CWE-760
(predictable salt). Severity depends on deployment, but these credentials typically grant
network access.

Suggested fix

1. app/operators/library/attributes.php

         case "Crypt-Password":
-            return crypt($value, 'SALT_DALORADIUS');
+            // crypt() picks the algorithm from the salt prefix. A salt that does not
+            // start with $ selects traditional DES: 8-character truncation and, here,
+            // a salt shared by every user. Use SHA-512 crypt with a per-user salt.
+            return crypt($value, '$6$' . bin2hex(random_bytes(8)));

All three call sites in the operators app only produce hashes, so a random salt is safe
there.

2. app/users/pref-auth-password-edit.php — needs more than the same edit

This copy also verifies the current password, by re-hashing it and comparing strings:

$current_hashed_password = hashPasswordAttribute($password_type, $current_password);
if ($current_hashed_password === false || $current_hashed_password !== $password_value) {
    continue;
}

That comparison only works because the salt is constant. Changing the salt here without
changing the comparison would break password changes for every user. Verification has to
re-use the stored salt:

$ok = ($password_type === "Crypt-Password")
    ? hash_equals($password_value, crypt($current_password, $password_value))
    : hash_equals($password_value, (string) hashPasswordAttribute($password_type, $current_password));
if (!$ok) {
    continue;
}

crypt($input, $stored) reads the algorithm and salt from $stored, so this keeps working
for existing DES hashes as well as new $6$ ones. Using hash_equals() also removes the
timing-comparison issue in the current code.

3. Existing installations

Stored DES hashes cannot be upgraded in place, since the plaintext is unrecoverable and
only 8 characters of it were ever used. Deployments will need a password reset for accounts
created with Crypt-Password. It would help operators to flag short/DES hashes in the UI —
they are 13 characters and begin with the fixed salt, so they are easy to detect:

SELECT username FROM radcheck
 WHERE attribute = 'Crypt-Password' AND value NOT LIKE '$%';

4. Compatibility

FreeRADIUS's rlm_pap delegates Crypt-Password to the system crypt(), so $6$ values
are verified without any FreeRADIUS-side change. Confirmed against FreeRADIUS 3.0.26.

Adjacent observation (not a vulnerability)

In app/users/pref-auth-password-edit.php the guard is

if (preg_match("/-Password$/", $attribute) === false) {

preg_match() returns 0 when the pattern does not match and false only on error, so
this guard never rejects a non-password attribute. The operators copy uses a helper
(is_passwordlike_attribute()) and does not have this issue.

Environment

daloRADIUS master @ 9b5b07847d28345270bba797e960cfec2fab3fce (2026-08-05)
PHP 8.1.2
OS Ubuntu 22.04.5 LTS (aarch64)
FreeRADIUS 3.0.26
Database MariaDB 10.6.23

How this was found

Encountered while deploying daloRADIUS as the management UI for a FreeRADIUS
server, when reviewing which Password Type to choose for new users. Every
claim above was executed against the running stack rather than inferred from
reading the code — the truncation, the shared salt, the proposed replacement,
and the backward compatibility of the suggested verification change.

Severity

Moderate

CVE ID

No known CVE

Weaknesses

Use of a One-Way Hash with a Predictable Salt

The product uses a one-way cryptographic hash against an input that should not be reversible, such as a password, but the product uses a predictable salt as part of the input. Learn more on MITRE.

Use of Password Hash With Insufficient Computational Effort

The product generates a hash for a password, but it uses a scheme that does not provide a sufficient level of computational effort that would make password cracking attacks infeasible or expensive. Learn more on MITRE.

Credits