main
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
/**
|
||||
* Forced password-change destination — Layer 2 of the default-password
|
||||
* protection (see config/banner_warnings.inc for the redirect logic).
|
||||
*
|
||||
* Reached only when the operator is connecting from a remote subnet
|
||||
* AND still using the factory default basic-auth password. Renders a
|
||||
* single-purpose form: change the password, nothing else.
|
||||
*
|
||||
* The htpasswd / chpasswd update flow is a verbatim mirror of the
|
||||
* password-change section in admin/configure.php (~line 440 onward).
|
||||
* Keep the two in sync — any future fix to one belongs in the other.
|
||||
*
|
||||
* On success: redirect to /admin/. The Layer 2 trigger will no longer
|
||||
* fire (password is no longer 'raspberry'), so the operator returns
|
||||
* to normal dashboard navigation.
|
||||
*
|
||||
* Direct-URL access by an operator who is NOT using the default
|
||||
* password just redirects to /admin/ — the page is only useful when
|
||||
* the trigger condition holds.
|
||||
*/
|
||||
|
||||
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/security_headers.php');
|
||||
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/csrf.php');
|
||||
require_once($_SERVER['DOCUMENT_ROOT'] . '/config/banner_warnings.inc');
|
||||
setSecurityHeaders();
|
||||
|
||||
// CSRF: rejects forged POSTs cleanly with 403 before any chpasswd /
|
||||
// htpasswd side-effect. Bootstraps the session on GET so the token
|
||||
// cookie ships with the form render.
|
||||
csrf_verify();
|
||||
|
||||
// If the operator's password is NOT the default, they don't need
|
||||
// this page. Send them to the main admin dashboard.
|
||||
//
|
||||
// Done BEFORE any HTML output so the redirect is clean.
|
||||
if (!_pistar_default_password_in_use()) {
|
||||
header('Location: /admin/', true, 302);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once('../config/language.php');
|
||||
$pistarReleaseConfig = '/etc/pistar-release';
|
||||
$configPistarRelease = parse_ini_file($pistarReleaseConfig, true);
|
||||
require_once('../config/version.php');
|
||||
|
||||
// Mirror of admin/configure.php password change (~line 440). Keep
|
||||
// these two paths byte-equivalent so a fix to one always lands in
|
||||
// the other. proc_open() + stdin pipes — password never on argv,
|
||||
// never reaches a shell. chpasswd FIRST so PAM rejection blocks
|
||||
// the htpasswd write and keeps web/shell auth in sync.
|
||||
//
|
||||
// Two-field verification: the form posts both `adminPassword` and
|
||||
// `adminPasswordConfirm`. Client-side functions.js compares them on
|
||||
// keystroke and only enables the submit button when they match;
|
||||
// the server-side check below is defence in depth — never trust
|
||||
// the client to enforce verification.
|
||||
$passwordChanged = false;
|
||||
$passwordRejected = false;
|
||||
$rejectReason = '';
|
||||
if (!empty($_POST['adminPassword'])) {
|
||||
$rawPassword = stripslashes(trim($_POST['adminPassword']));
|
||||
$rawPasswordConfirm = isset($_POST['adminPasswordConfirm'])
|
||||
? stripslashes(trim($_POST['adminPasswordConfirm']))
|
||||
: '';
|
||||
|
||||
$hasIllegalChar = preg_match('/[\x00\r\n]/', $rawPassword);
|
||||
$tooLong = strlen($rawPassword) > 256;
|
||||
$isStillDefault = hash_equals('raspberry', $rawPassword);
|
||||
// hash_equals on user-supplied vs user-supplied isn't a security
|
||||
// boundary (no secret on either side), but it's the right idiom
|
||||
// for password compares — and guards against any future caller
|
||||
// mistaking the result for an authentication signal.
|
||||
$confirmMatches = hash_equals($rawPassword, $rawPasswordConfirm);
|
||||
|
||||
if ($rawPassword === '' || $hasIllegalChar || $tooLong) {
|
||||
$passwordRejected = true;
|
||||
$rejectReason = 'invalid';
|
||||
error_log('Pi-Star change_password_required.php: adminPassword rejected '
|
||||
. '(empty, contains NUL/CR/LF, or > 256 bytes)');
|
||||
} elseif ($isStillDefault) {
|
||||
$passwordRejected = true;
|
||||
$rejectReason = 'default';
|
||||
error_log('Pi-Star change_password_required.php: adminPassword rejected (still the default)');
|
||||
} elseif (!$confirmMatches) {
|
||||
$passwordRejected = true;
|
||||
$rejectReason = 'mismatch';
|
||||
error_log('Pi-Star change_password_required.php: adminPassword rejected '
|
||||
. '(confirmation does not match)');
|
||||
} else {
|
||||
// Pi-Star runs with `/` mounted read-only by default. chpasswd
|
||||
// and htpasswd both write to files on the rootfs (/etc/shadow
|
||||
// and /var/www/.htpasswd respectively); without a remount-rw
|
||||
// they fail with PAM "Authentication token manipulation error"
|
||||
// and the symptom looks like every password is rejected. The
|
||||
// matching configure.php POST handler does this at line ~348.
|
||||
// Re-seal the rootfs at the bottom of this block on every
|
||||
// exit path (success, PAM rejection, htpasswd failure).
|
||||
system('sudo mount -o remount,rw /');
|
||||
|
||||
$descriptors = array(
|
||||
0 => array('pipe', 'r'),
|
||||
1 => array('pipe', 'w'),
|
||||
2 => array('pipe', 'w'),
|
||||
);
|
||||
|
||||
// 1. Linux user via chpasswd. Hardcoded command; payload on stdin only.
|
||||
$cpExit = 1;
|
||||
$cpStderr = '';
|
||||
$proc = proc_open('sudo -n /usr/sbin/chpasswd', $descriptors, $pipes);
|
||||
if (is_resource($proc)) {
|
||||
fwrite($pipes[0], 'pi-star:' . $rawPassword . "\n");
|
||||
fclose($pipes[0]);
|
||||
stream_get_contents($pipes[1]); fclose($pipes[1]);
|
||||
$cpStderr = stream_get_contents($pipes[2]); fclose($pipes[2]);
|
||||
$cpExit = proc_close($proc);
|
||||
} else {
|
||||
error_log('Pi-Star change_password_required.php: failed to spawn chpasswd');
|
||||
}
|
||||
|
||||
if ($cpExit !== 0) {
|
||||
// PAM rejection (libpwquality, history, length, etc.).
|
||||
$passwordRejected = true;
|
||||
$rejectReason = 'pam';
|
||||
error_log('Pi-Star change_password_required.php: chpasswd exit='
|
||||
. $cpExit . '; stderr=' . trim($cpStderr));
|
||||
} else {
|
||||
// 2. Apache basic-auth file. /var/www/.htpasswd is owned by
|
||||
// www-data, so no sudo needed.
|
||||
$htExit = 1;
|
||||
$htStderr = '';
|
||||
$proc = proc_open('htpasswd -i /var/www/.htpasswd pi-star',
|
||||
$descriptors, $pipes);
|
||||
if (is_resource($proc)) {
|
||||
fwrite($pipes[0], $rawPassword);
|
||||
fclose($pipes[0]);
|
||||
stream_get_contents($pipes[1]); fclose($pipes[1]);
|
||||
$htStderr = stream_get_contents($pipes[2]); fclose($pipes[2]);
|
||||
$htExit = proc_close($proc);
|
||||
if ($htExit !== 0) {
|
||||
error_log('Pi-Star change_password_required.php: htpasswd exit='
|
||||
. $htExit . ' (Linux password updated); stderr='
|
||||
. trim($htStderr));
|
||||
}
|
||||
} else {
|
||||
error_log('Pi-Star change_password_required.php: failed to spawn htpasswd');
|
||||
}
|
||||
// Tighten /var/www/.htpasswd from htpasswd's default 644
|
||||
// to 600. Owner stays www-data so nginx (running as
|
||||
// www-data) keeps reading for basic-auth checks; other
|
||||
// local users no longer see the bcrypt hash.
|
||||
//
|
||||
// Surface chmod failure into the error log. The realistic
|
||||
// failure modes are (a) htpasswd spawn failed and the
|
||||
// file is in some unexpected state, or (b) a future
|
||||
// refactor moves this call outside the rootfs-rw window
|
||||
// (it sits inside one today — see commit aa999d49).
|
||||
// Either way, the file silently staying mode 644 would
|
||||
// be worth knowing about.
|
||||
if (@chmod('/var/www/.htpasswd', 0600) === false) {
|
||||
error_log('Pi-Star change_password_required.php: chmod 0600 /var/www/.htpasswd failed (file may stay mode 644 until next save)');
|
||||
}
|
||||
// chpasswd succeeded; treat as success even if htpasswd
|
||||
// failed (operator can retry — same behaviour as
|
||||
// configure.php). The browser will re-prompt for the new
|
||||
// credentials on the next request.
|
||||
$passwordChanged = true;
|
||||
}
|
||||
|
||||
// Re-seal the rootfs on every exit path out of the writable
|
||||
// block. sync first so the shadow / htpasswd writes land on
|
||||
// disk before the remount makes the FS read-only again.
|
||||
system('sudo sync && sudo sync && sudo sync && sudo mount -o remount,ro /');
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="Author" content="Andrew Taylor (MW0MWZ)" />
|
||||
<meta name="Description" content="Pi-Star — Default password change required" />
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
|
||||
<link rel="stylesheet" type="text/css" href="../css/pistar-css.php" />
|
||||
<script type="text/javascript" src="/functions.js"></script>
|
||||
<title>Pi-Star — Change Default Password</title>
|
||||
</head>
|
||||
<body>
|
||||
<?php pistar_warnings_render(); ?>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Pi-Star — Default Password Change Required</h1>
|
||||
</div>
|
||||
<div class="contentwide">
|
||||
<?php if ($passwordChanged) { ?>
|
||||
<table>
|
||||
<tr><th>Working...</th></tr>
|
||||
<tr><td>Password changed. Your browser will re-prompt for the new credentials shortly.</td></tr>
|
||||
</table>
|
||||
<script type="text/javascript">setTimeout(function() { window.location='/admin/'; }, 5000);</script>
|
||||
<?php } else { ?>
|
||||
<p>You are connecting from outside the local network and your dashboard is still using the
|
||||
factory default password. Set a new password to continue.</p>
|
||||
<?php if ($passwordRejected) {
|
||||
// Distinct message per rejection class so the operator knows
|
||||
// exactly what to fix without having to guess.
|
||||
switch ($rejectReason) {
|
||||
case 'mismatch':
|
||||
$msg = '<b>Passwords do not match.</b> Please enter the same value in both fields.';
|
||||
break;
|
||||
case 'default':
|
||||
$msg = '<b>Password rejected.</b> The new password must be different from the factory default.';
|
||||
break;
|
||||
case 'pam':
|
||||
$msg = '<b>Password rejected by the system.</b> It must satisfy the system password-quality rules (length, complexity, history).';
|
||||
break;
|
||||
case 'invalid':
|
||||
default:
|
||||
$msg = '<b>Password rejected.</b> It must not contain NUL / CR / LF bytes and must be at most 256 bytes long.';
|
||||
break;
|
||||
}
|
||||
?>
|
||||
<p style="color: #f01010;"><?php echo $msg; ?></p>
|
||||
<?php } ?>
|
||||
<form id="adminPassForm" action="/admin/change_password_required.php" method="post" autocomplete="off">
|
||||
<?php echo csrf_field_html(); ?>
|
||||
<table>
|
||||
<tr>
|
||||
<td align="right" width="40%"><label for="pass1">New password:</label></td>
|
||||
<td align="left">
|
||||
<input type="password" name="adminPassword" id="pass1"
|
||||
size="32" autocomplete="new-password"
|
||||
onkeyup="checkPass(); return false;" required="required" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right"><label for="pass2">Confirm password:</label></td>
|
||||
<td align="left">
|
||||
<input type="password" name="adminPasswordConfirm" id="pass2"
|
||||
size="32" autocomplete="new-password"
|
||||
onkeyup="checkPass(); return false;" required="required" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="center">
|
||||
<!-- id="submitpwd" matches the existing checkPass() in
|
||||
functions.js which enables this button only when both
|
||||
fields match. Server still re-verifies, so the JS gate
|
||||
is UX, not security. -->
|
||||
<input type="submit" id="submitpwd" value="Change Password" disabled="disabled" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="footer">
|
||||
Pi-Star / Pi-Star Dashboard, © Andy Taylor (MW0MWZ) 2014-<?php echo date('Y'); ?>.<br />
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user