PHP WebShell
Текущая директория: /var/www/bitcardoApp/backyard/models/security
Просмотр файла: 2fa.php
<?php
// backyard/models/security/2fa.php
// Helper functions for 2FA/TOTP admin. Procedural MySQLi, schema-adaptive and null-safe.
// Simplified: never write NULL into secret_base32 (use empty string '') to avoid NOT NULL errors.
if (!function_exists('h')) {
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
}
/** Return column names for a table. */
function fa_describe(mysqli $conn, string $table): array {
$cols = [];
$t = mysqli_real_escape_string($conn, $table);
if ($res = mysqli_query($conn, "DESCRIBE `{$t}`")) {
while ($r = mysqli_fetch_assoc($res)) $cols[] = $r['Field'];
mysqli_free_result($res);
}
return $cols;
}
/** Check if a table exists. */
function fa_table_exists(mysqli $conn, string $table): bool {
$t = mysqli_real_escape_string($conn, $table);
if ($res = mysqli_query($conn, "SHOW TABLES LIKE '{$t}'")) {
$ok = mysqli_num_rows($res) > 0;
mysqli_free_result($res);
return $ok;
}
return false;
}
/**
* Check whether a column is nullable in the given table.
* Returns true if column exists and IS_NULLABLE = 'YES'.
*/
function fa_column_nullable(mysqli $conn, string $table, string $column): bool {
$table_safe = mysqli_real_escape_string($conn, $table);
$col_safe = mysqli_real_escape_string($conn, $column);
// get current database name
$dbrow = mysqli_fetch_row(mysqli_query($conn, "SELECT DATABASE()"));
$db_name = $dbrow[0] ?? '';
if ($db_name === '') return false;
$db_safe = mysqli_real_escape_string($conn, $db_name);
$sql = "
SELECT IS_NULLABLE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = '{$db_safe}' AND TABLE_NAME = '{$table_safe}' AND COLUMN_NAME = '{$col_safe}' LIMIT 1
";
if ($res = mysqli_query($conn, $sql)) {
$row = mysqli_fetch_assoc($res);
mysqli_free_result($res);
return isset($row['IS_NULLABLE']) && strtoupper($row['IS_NULLABLE']) === 'YES';
}
return false;
}
/**
* Helper that returns SQL expression for assigning NULL or empty string depending on column nullability.
* Special-case: secret_base32 will always use empty string '' (never NULL) to avoid NOT NULL constraint errors.
*/
function fa_assign_null_or_empty_sql(mysqli $conn, string $table, string $column): string {
$col_safe = mysqli_real_escape_string($conn, $column);
// special-case: never set secret_base32 to NULL; use empty string
if (strtolower($column) === 'secret_base32' || strtolower($column) === 'secret_base32') {
return "{$col_safe}=''";
}
$nullable = fa_column_nullable($conn, $table, $column);
if ($nullable) {
return "{$col_safe}=NULL";
} else {
return "{$col_safe}=''";
}
}
/**
* List users with TOTP info.
*/
function fa_list_users(mysqli $conn, array $filters): array {
$page = max(1, (int)($filters['page'] ?? 1));
$per = max(1, min(100, (int)($filters['per_page'] ?? 25)));
$off = ($page-1)*$per;
$w = ["1=1"];
$q = trim((string)($filters['q'] ?? ''));
if ($q !== '') {
$qSafe = mysqli_real_escape_string($conn, "%{$q}%");
$w[] = "(u.first_name LIKE '{$qSafe}' OR u.last_name LIKE '{$qSafe}' OR u.email LIKE '{$qSafe}' OR u.phone LIKE '{$qSafe}')";
}
$status = strtolower(trim((string)($filters['status'] ?? 'all')));
if ($status === 'active' || $status === 'inactive') {
$st = mysqli_real_escape_string($conn, $status);
$w[] = "LOWER(u.user_status) = '{$st}'";
}
$totp_join = '';
$select_totp_enabled = '0 AS totp_enabled';
$select_totp_created = 'NULL AS totp_created_at';
$select_totp_verified = 'NULL AS totp_verified_at';
$only_totp = (int)($filters['only_totp'] ?? 0);
if (fa_table_exists($conn, 'user_totp')) {
$totp_join = "LEFT JOIN user_totp ut ON ut.user_id = u.user_id";
$utCols = fa_describe($conn, 'user_totp');
$hasEnabled = in_array('enabled', $utCols, true);
$hasSecret = in_array('secret_base32', $utCols, true);
$hasCreated = in_array('created_at', $utCols, true);
$hasVerified= in_array('verified_at', $utCols, true);
if ($hasEnabled) {
$select_totp_enabled = "COALESCE(ut.enabled,0) AS totp_enabled";
if ($only_totp === 1) $w[] = "(ut.enabled = 1)";
} elseif ($hasSecret) {
$select_totp_enabled = "CASE WHEN ut.user_id IS NULL THEN 0 WHEN ut.secret_base32 IS NULL OR ut.secret_base32='' THEN 0 ELSE 1 END AS totp_enabled";
if ($only_totp === 1) $w[] = "(ut.secret_base32 IS NOT NULL AND ut.secret_base32<>'')";
} else {
$select_totp_enabled = "CASE WHEN ut.user_id IS NULL THEN 0 ELSE 1 END AS totp_enabled";
if ($only_totp === 1) $w[] = "(ut.user_id IS NOT NULL)";
}
$select_totp_created = $hasCreated ? "ut.created_at AS totp_created_at" : "NULL AS totp_created_at";
$select_totp_verified= $hasVerified ? "ut.verified_at AS totp_verified_at" : "NULL AS totp_verified_at";
} else {
if ($only_totp === 1) $w[] = "0=1"; // none
}
$where = 'WHERE '.implode(' AND ', $w);
// Count
$sqlTotal = "SELECT COUNT(*) AS c
FROM users u
{$totp_join}
{$where}";
$total = 0;
if ($res = mysqli_query($conn, $sqlTotal)) {
$row = mysqli_fetch_assoc($res);
$total = (int)($row['c'] ?? 0);
mysqli_free_result($res);
}
$pages = max(1, (int)ceil($total / $per));
// Rows
$sql = "SELECT
u.user_id, u.first_name, u.last_name, u.email, u.phone, u.user_status,
{$select_totp_enabled},
{$select_totp_created},
{$select_totp_verified}
FROM users u
{$totp_join}
{$where}
ORDER BY u.user_id DESC
LIMIT {$per} OFFSET {$off}";
$rows = [];
if ($res = mysqli_query($conn, $sql)) {
while ($r = mysqli_fetch_assoc($res)) $rows[] = $r;
mysqli_free_result($res);
}
return ['rows'=>$rows, 'total'=>$total, 'page'=>$page, 'pages'=>$pages, 'per_page'=>$per];
}
/**
* Enable/Disable TOTP. If no `enabled` column, enabling = ensure row; disabling = wipe secret_base32.
*/
function fa_set_totp_enabled(mysqli $conn, int $user_id, bool $enabled): bool {
$uid = (int)$user_id;
if (!fa_table_exists($conn, 'user_totp')) return false;
$utCols = fa_describe($conn, 'user_totp');
$hasEnabled = in_array('enabled', $utCols, true);
$hasSecret = in_array('secret_base32', $utCols, true);
$hasVerified= in_array('verified_at', $utCols, true);
// ensure row exists
$exists = false;
if ($r = mysqli_query($conn, "SELECT user_id FROM user_totp WHERE user_id={$uid} LIMIT 1")) {
$exists = mysqli_num_rows($r) > 0; mysqli_free_result($r);
}
if (!$exists) {
// Build insert columns/values depending on existing columns.
// IMPORTANT: secret_base32 will be inserted as empty string '' (never NULL).
$cols = ['user_id'];
$vals = ["{$uid}"];
if ($hasSecret) {
$cols[] = 'secret_base32';
$vals[] = "''";
}
if ($hasEnabled) {
$cols[] = 'enabled';
$vals[] = '0';
}
if ($hasVerified) {
$cols[] = 'verified_at';
$vals[] = 'NULL';
}
$sql = "INSERT INTO user_totp (".implode(',', $cols).") VALUES (".implode(',', $vals).")";
if (!mysqli_query($conn, $sql)) return false;
}
if ($hasEnabled) {
$flag = $enabled ? 1 : 0;
$setParts = ["enabled={$flag}"];
if (!$enabled && $hasSecret) {
// wipe secret to empty string (never NULL)
$setParts[] = "secret_base32=''";
}
if (!$enabled && $hasVerified) {
// clear verified_at (set NULL when possible)
$nullable = fa_column_nullable($conn, 'user_totp', 'verified_at');
$setParts[] = $nullable ? "verified_at=NULL" : "verified_at='0000-00-00 00:00:00'";
}
$setSql = implode(',', $setParts);
return (bool)mysqli_query($conn, "UPDATE user_totp SET {$setSql} WHERE user_id={$uid} LIMIT 1");
}
// No enabled column -> emulate behavior
if ($enabled) {
return true;
} else {
return fa_reset_totp($conn, $uid);
}
}
/** Reset TOTP (wipe secret_base32 to empty string, set enabled=0 if present, clear verified_at if present). */
function fa_reset_totp(mysqli $conn, int $user_id): bool {
$uid = (int)$user_id;
if (!fa_table_exists($conn, 'user_totp')) return false;
$utCols = fa_describe($conn, 'user_totp');
$hasEnabled = in_array('enabled', $utCols, true);
$hasSecret = in_array('secret_base32', $utCols, true);
$hasVerified= in_array('verified_at', $utCols, true);
$sets = [];
if ($hasSecret) {
// always set empty string (never NULL)
$sets[] = "secret_base32=''";
}
if ($hasEnabled) {
$sets[] = "enabled=0";
}
if ($hasVerified) {
$nullable = fa_column_nullable($conn, 'user_totp', 'verified_at');
$sets[] = $nullable ? "verified_at=NULL" : "verified_at='0000-00-00 00:00:00'";
}
if (empty($sets)) return true;
// Also clear one-time codes if you store any
@mysqli_query($conn, "DELETE FROM user_otps WHERE user_id={$uid}");
$sql = "UPDATE user_totp SET ".implode(',', $sets)." WHERE user_id={$uid} LIMIT 1";
return (bool)mysqli_query($conn, $sql);
}
/** Regenerate backup codes (return plaintext once; store SHA-256 in code_hash). */
function fa_regenerate_backup_codes(mysqli $conn, int $user_id, int $count=10): array {
$uid = (int)$user_id;
if (!fa_table_exists($conn, 'user_backup_codes')) return [];
// wipe old codes
@mysqli_query($conn, "DELETE FROM user_backup_codes WHERE user_id={$uid}");
$codes = [];
for ($i=0; $i<$count; $i++) {
$plain = strtoupper(substr(bin2hex(random_bytes(5)), 0, 8)); // e.g., 'A1B2C3D4'
$hash = hash('sha256', $plain);
$hashSafe = mysqli_real_escape_string($conn, $hash);
mysqli_query($conn, "INSERT INTO user_backup_codes (user_id, code_hash) VALUES ({$uid}, '{$hashSafe}')");
$codes[] = $plain;
}
return $codes;
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!