Fix totp_setup.php to use header-based CSRF token verification

This commit is contained in:
Ryan
2025-04-11 23:40:27 -04:00
committed by GitHub
parent b06c49f213
commit 28ac23c2f6
3 changed files with 82 additions and 29 deletions

View File

@@ -14,6 +14,7 @@
- Updated TOTP integration (namespace, enum, QR provider) accordingly - Updated TOTP integration (namespace, enum, QR provider) accordingly
- Updated docker image from 22.04 to 24.04 <https://github.com/error311/filerise-docker> - Updated docker image from 22.04 to 24.04 <https://github.com/error311/filerise-docker>
- Ensure consistent session behavior - Ensure consistent session behavior
- Fix totp_setup.php to use header-based CSRF token verification
--- ---

View File

@@ -325,19 +325,21 @@ export function openTOTPModal() {
z-index: 3100; z-index: 3100;
`; `;
totpModal.innerHTML = ` totpModal.innerHTML = `
<div class="modal-content" style="${modalContentStyles}"> <div class="modal-content" style="${modalContentStyles}">
<span id="closeTOTPModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">&times;</span> <span id="closeTOTPModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">&times;</span>
<h3>TOTP Setup</h3> <h3>TOTP Setup</h3>
<p>Scan this QR code with your authenticator app:</p> <p>Scan this QR code with your authenticator app:</p>
<img src="totp_setup.php?csrf=${encodeURIComponent(window.csrfToken)}" alt="TOTP QR Code" style="max-width: 100%; height: auto; display: block; margin: 0 auto;"> <!-- Create an image placeholder without the CSRF token in the src -->
<br/> <img id="totpQRCodeImage" src="" alt="TOTP QR Code" style="max-width: 100%; height: auto; display: block; margin: 0 auto;">
<p>Enter the 6-digit code from your app to confirm setup:</p> <br/>
<input type="text" id="totpConfirmInput" maxlength="6" style="font-size:24px; text-align:center; width:100%; padding:10px;" placeholder="6-digit code" /> <p>Enter the 6-digit code from your app to confirm setup:</p>
<br/><br/> <input type="text" id="totpConfirmInput" maxlength="6" style="font-size:24px; text-align:center; width:100%; padding:10px;" placeholder="6-digit code" />
<button type="button" id="confirmTOTPBtn" class="btn btn-primary">Confirm</button> <br/><br/>
</div> <button type="button" id="confirmTOTPBtn" class="btn btn-primary">Confirm</button>
`; </div>
`;
document.body.appendChild(totpModal); document.body.appendChild(totpModal);
loadTOTPQRCode();
document.getElementById("closeTOTPModal").addEventListener("click", () => { document.getElementById("closeTOTPModal").addEventListener("click", () => {
closeTOTPModal(true); closeTOTPModal(true);
@@ -406,6 +408,13 @@ export function openTOTPModal() {
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff"; modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";
modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000"; modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000";
// Clear any previous QR code src if needed and then load it:
const qrImg = document.getElementById("totpQRCodeImage");
if (qrImg) {
qrImg.src = "";
}
loadTOTPQRCode();
// Focus the input and attach enter key listener // Focus the input and attach enter key listener
const totpConfirmInput = document.getElementById("totpConfirmInput"); const totpConfirmInput = document.getElementById("totpConfirmInput");
if (totpConfirmInput) { if (totpConfirmInput) {
@@ -419,6 +428,33 @@ export function openTOTPModal() {
} }
} }
function loadTOTPQRCode() {
fetch("totp_setup.php", {
method: "GET",
credentials: "include",
headers: {
"X-CSRF-Token": window.csrfToken // Send your CSRF token here
}
})
.then(response => {
if (!response.ok) {
throw new Error("Failed to fetch QR code. Status: " + response.status);
}
return response.blob();
})
.then(blob => {
const imageURL = URL.createObjectURL(blob);
const qrImg = document.getElementById("totpQRCodeImage");
if (qrImg) {
qrImg.src = imageURL;
}
})
.catch(error => {
console.error("Error loading TOTP QR code:", error);
showToast("Error loading QR code.");
});
}
// Updated closeTOTPModal function with a disable parameter // Updated closeTOTPModal function with a disable parameter
export function closeTOTPModal(disable = true) { export function closeTOTPModal(disable = true) {
const totpModal = document.getElementById("totpModal"); const totpModal = document.getElementById("totpModal");
@@ -800,18 +836,18 @@ function loadUserPermissionsList() {
if ((user.role && user.role === "1") || user.username.toLowerCase() === "admin") return; if ((user.role && user.role === "1") || user.username.toLowerCase() === "admin") return;
// Use stored permissions if available; otherwise fall back to localStorage defaults. // Use stored permissions if available; otherwise fall back to localStorage defaults.
const defaultPerm = { const defaultPerm = {
folderOnly: false, folderOnly: false,
readOnly: false, readOnly: false,
disableUpload: false, disableUpload: false,
}; };
// Normalize the username key to match server storage (e.g., lowercase) // Normalize the username key to match server storage (e.g., lowercase)
const usernameKey = user.username.toLowerCase(); const usernameKey = user.username.toLowerCase();
const userPerm = (permissionsData && typeof permissionsData === "object" && (usernameKey in permissionsData)) const userPerm = (permissionsData && typeof permissionsData === "object" && (usernameKey in permissionsData))
? permissionsData[usernameKey] ? permissionsData[usernameKey]
: defaultPerm; : defaultPerm;
// Create a row for the user. // Create a row for the user.
const row = document.createElement("div"); const row = document.createElement("div");

View File

@@ -9,16 +9,34 @@ use Endroid\QrCode\Writer\PngWriter;
use RobThree\Auth\Algorithm; use RobThree\Auth\Algorithm;
use RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider; use RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider;
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { // Define the respond() helper if not already defined.
if (!function_exists('respond')) {
function respond($status, $code, $message, $data = []) {
http_response_code($code);
echo json_encode([
'status' => $status,
'code' => $code,
'message' => $message,
'data' => $data
]);
exit;
}
}
// Allow access if the user is authenticated or pending TOTP.
if (!((isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true) || isset($_SESSION['pending_login_user']))) {
http_response_code(403); http_response_code(403);
exit; exit;
} }
// Retrieve CSRF token from GET parameter or request headers.
$headers = array_change_key_case(getallheaders(), CASE_LOWER); $headers = array_change_key_case(getallheaders(), CASE_LOWER);
$csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; $receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) { if ($receivedToken !== $_SESSION['csrf_token']) {
respond('error', 403, 'Invalid CSRF token'); echo json_encode(["error" => "Invalid CSRF token"]);
http_response_code(403);
exit;
} }
$username = $_SESSION['username'] ?? ''; $username = $_SESSION['username'] ?? '';
@@ -111,7 +129,7 @@ $tfa = new \RobThree\Auth\TwoFactorAuth(
'FileRise', // issuer 'FileRise', // issuer
6, // number of digits 6, // number of digits
30, // period in seconds 30, // period in seconds
Algorithm::Sha1 // Correct enum case name from your enum Algorithm::Sha1 // enum case from your Algorithm enum
); );
// Retrieve the current TOTP secret for the user. // Retrieve the current TOTP secret for the user.
@@ -124,8 +142,6 @@ if (!$totpSecret) {
} }
// Determine the otpauth URL to use. // Determine the otpauth URL to use.
// If a global OTPAuth URL template is defined, replace placeholders {label} and {secret}.
// Otherwise, use the default method.
$globalOtpauthUrl = getGlobalOtpauthUrl(); $globalOtpauthUrl = getGlobalOtpauthUrl();
if (!empty($globalOtpauthUrl)) { if (!empty($globalOtpauthUrl)) {
$label = "FileRise:" . $username; $label = "FileRise:" . $username;