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;
}

Выполнить команду


Для локальной разработки. Не используйте в интернете!