Summary
Crypt-Password values written by daloRADIUS are hashed with traditional DES crypt(3)
using a single hard-coded salt. This has three consequences:
- 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.
- Every user shares the same salt (
SA), so one precomputed table breaks all accounts
at once, and identical passwords are visible as identical hashes.
- 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.php — hashPasswordAttribute() |
hashing on create/update |
app/users/pref-auth-password-edit.php — hashPasswordAttribute() |
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.
Summary
Crypt-Passwordvalues written by daloRADIUS are hashed with traditional DEScrypt(3)using a single hard-coded salt. This has three consequences:
as the hash of its first 8 characters, and authenticates with only those 8 characters.
SA), so one precomputed table breaks all accountsat once, and identical passwords are visible as identical hashes.
crypt(3)is computationally trivial to brute force on modern hardware.The affected values live in the RADIUS
radchecktable, i.e. the credentials thatauthenticate users to the networks daloRADIUS manages.
Operators who deliberately choose
Crypt-PasswordoverCleartext-Passwordare choosingthe option that looks safest in the UI, so the weakness is not visible to them.
Status: still present on current
masterVerified against
raw.githubusercontent.com/lirantal/daloradius/masterat the time ofwriting — the
Crypt-Passwordcase is unchanged.This is worth flagging because the function was edited recently: "feat: add SHA2 password
support (#716)" (2026-06-22) added a
SHA2-Passwordcase immediately below the affectedline, 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):app/operators/library/attributes.php—hashPasswordAttribute()app/users/pref-auth-password-edit.php—hashPasswordAttribute()PHP's
crypt()selects the algorithm from the salt prefix. BecauseSALT_DALORADIUSdoes not begin with
$, it is interpreted as a traditional DES salt, and only its firsttwo characters (
SA) are used.Reached from at least:
app/operators/mng-new.php(viaattributes.php:234)app/operators/mng-batch-add.php:287,295app/operators/mng-import-users.php:227app/users/pref-auth-password-edit.php:127,132Reproduction
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) usingonly the first 8 characters.
Impact
An attacker who obtains a copy of
radcheck— a database backup, a SQL injection, acompromised 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.phpAll 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 editThis copy also verifies the current password, by re-hashing it and comparing strings:
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:
crypt($input, $stored)reads the algorithm and salt from$stored, so this keeps workingfor existing DES hashes as well as new
$6$ones. Usinghash_equals()also removes thetiming-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:
4. Compatibility
FreeRADIUS's
rlm_papdelegatesCrypt-Passwordto the systemcrypt(), so$6$valuesare verified without any FreeRADIUS-side change. Confirmed against FreeRADIUS 3.0.26.
Adjacent observation (not a vulnerability)
In
app/users/pref-auth-password-edit.phpthe guard ispreg_match()returns0when the pattern does not match andfalseonly on error, sothis guard never rejects a non-password attribute. The operators copy uses a helper
(
is_passwordlike_attribute()) and does not have this issue.Environment
master@9b5b07847d28345270bba797e960cfec2fab3fce(2026-08-05)How this was found
Encountered while deploying daloRADIUS as the management UI for a FreeRADIUS
server, when reviewing which
Password Typeto choose for new users. Everyclaim 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.