web app security fix
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 644 KiB After Width: | Height: | Size: 138 KiB |
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
//<><><><><><><><> MAIN TRCLOUD <><><><><><><><>//
|
||||
if(true){
|
||||
$isTest = "master";
|
||||
$server_url = "/mn3wms/app/"; // URL path to the app root
|
||||
$include_url = $_SERVER['DOCUMENT_ROOT']."/mn3wms/app/";
|
||||
|
||||
$db_server = "localhost"; // usually localhost
|
||||
$db_user = "root"; // usually root
|
||||
$db_pass = "CHANGE_ME"; // database password
|
||||
$db_type = "mysql"; // database type
|
||||
$db_database = "wms"; // database name main
|
||||
|
||||
//~ $db_server2 = "";
|
||||
//~ $db_user2 = "";
|
||||
//~ $db_pass2 = "";
|
||||
//~ $db_type2 = "";
|
||||
$db_database2 = "wms2"; // database for client
|
||||
|
||||
$db_server2 = (!empty($db_server2))?$db_server2:$db_server;
|
||||
$db_user2 = (!empty($db_user2))?$db_user2:$db_user;
|
||||
$db_pass2 = (!empty($db_pass2))?$db_pass2:$db_pass;
|
||||
$db_type2 = (!empty($db_type2))?$db_type2:$db_type;
|
||||
|
||||
$time_zone = "Asia/Bangkok";
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// unique key
|
||||
$pinkey = "wms";
|
||||
/**
|
||||
* default smtp server
|
||||
*/
|
||||
$SMTP = [];
|
||||
$SMTP['server'] = "smtp.gmail.com";
|
||||
$SMTP['username'] = "your_email@gmail.com"; // Gmail address
|
||||
$SMTP['port'] = "587";
|
||||
$SMTP['password'] = "CHANGE_ME"; // Gmail app password (not your login password)
|
||||
// password encoding
|
||||
$method = "AES-256-CBC";
|
||||
$iv = "1234567890123456"; // Must be exactly 16 bytes
|
||||
$SMTP['password'] = openssl_encrypt($SMTP['password'], $method, $pinkey, 0, $iv);
|
||||
@@ -616,10 +616,12 @@
|
||||
var from_wh = data.ref.ref_warehouse;
|
||||
var to_wh = data.ref_warehouse;
|
||||
|
||||
// Lock lot/serial
|
||||
$('#lot_number').html('<option value="' + (data.lot_number || '') + '">' + (data.lot_number || '—') + '</option>')
|
||||
// Lock lot/serial — use DOM construction (not .html()) to prevent XSS
|
||||
$('#lot_number').empty()
|
||||
.append($('<option>').val(data.lot_number || '').text(data.lot_number || '—'))
|
||||
.val(data.lot_number || '').prop('disabled', true);
|
||||
$('#serial_number').html('<option value="' + (data.serial_number || '') + '">' + (data.serial_number || '—') + '</option>')
|
||||
$('#serial_number').empty()
|
||||
.append($('<option>').val(data.serial_number || '').text(data.serial_number || '—'))
|
||||
.val(data.serial_number || '').prop('disabled', true);
|
||||
|
||||
$('#product_name').val(data.product_name);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<footer class="text-center py-2 mt-6 text-secondary ">
|
||||
<p class="mb-0">Copyright © 2026 WMS. Developed by <a href="https://codescandy.com/"
|
||||
<p class="mb-0">Copyright © 2026 MN3WMS. Developed by <a href="https://codescandy.com/"
|
||||
target="_blank" class="text-primary">TR3</a> </p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
<?php
|
||||
// Security headers — emitted before any HTML output.
|
||||
// X-Content-Type-Options: prevents MIME-sniffing attacks.
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
// X-Frame-Options: blocks this page from being embedded in a cross-origin iframe.
|
||||
header('X-Frame-Options: SAMEORIGIN');
|
||||
// Referrer-Policy: sends origin only on same-origin; omits on cross-origin navigations.
|
||||
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
@@ -12,7 +9,7 @@ header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>TRx WMS</title>
|
||||
<title>MN3 WMS</title>
|
||||
<meta name="csrf-token" content="<?= htmlspecialchars($_SESSION['csrf_token'] ?? '') ?>">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="<?php echo $server_url?>assets/images/favicon.png">
|
||||
@@ -23,8 +20,14 @@ header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
<!-- jquery -->
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.js" integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4=" crossorigin="anonymous"></script>
|
||||
|
||||
<!-- popper (must be before bootstrap) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" crossorigin="anonymous"></script>
|
||||
|
||||
<!-- bootstrap -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.min.js" integrity="sha384-G/EV+4j2dNv+tEPo3++6LCgdCROaejBqfUeNjuKAiuXbjrxilcCdDz6ZAVfHWe1Y" crossorigin="anonymous"></script>
|
||||
<!-- Disable CDN Bootstrap's dropdown toggle — main.js bundles Bootstrap+Popper and
|
||||
handles dropdown events. CDN Bootstrap stays for window.bootstrap (Modal API). -->
|
||||
<script>bootstrap.Dropdown.prototype.toggle = function() {};</script>
|
||||
|
||||
<!-- bootbox -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.4.0/bootbox.min.js"></script>
|
||||
@@ -61,8 +64,6 @@ header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
|
||||
<!-- theme script -->
|
||||
<link rel="stylesheet" href="<?php echo $server_url?>assets/css/main.css">
|
||||
<!-- Popper UMD: must load before main.js so window.Popper exists globally -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" crossorigin="anonymous"></script>
|
||||
<script type="module" src="<?php echo $server_url?>assets/js/main.js"></script>
|
||||
<link rel="stylesheet" href="<?php echo $server_url?>assets/css/custom.css">
|
||||
<script src="<?php echo $server_url?>assets/js/custom.js"></script>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<?php
|
||||
|
||||
// logout once session expire
|
||||
if(!isset($_SESSION["otp"])){
|
||||
// Redirect to login if the user has not completed full authentication.
|
||||
// login_company_id is only written by login_confirm.php after OTP is verified —
|
||||
// using it (not "otp") ensures half-logged-in sessions are also redirected.
|
||||
if(empty($_SESSION["login_company_id"])){
|
||||
header('Location: '.$server_url.'login/index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
require 'preset.php';
|
||||
|
||||
@@ -70,14 +70,37 @@ $user_id = $sth->fetchColumn();
|
||||
|
||||
$username = strtolower($data["username"]);
|
||||
|
||||
// ── Step 2: Fetch the user's hashed password ──────────────────────────────────
|
||||
$sth = $pdo1->prepare("select password from user where username = ? or email = ? limit 1;");
|
||||
// ── Step 2: Fetch the user's hashed password + lockout state ─────────────────
|
||||
$sth = $pdo1->prepare("SELECT password, login_attempts, locked_until FROM user WHERE username = ? OR email = ? LIMIT 1;");
|
||||
$sth->execute(array($username, $username));
|
||||
$temp = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// ── Step 2a: Lockout check — only when the username resolves to a real user ──
|
||||
// We only block here when $user_id is set (valid username) to avoid leaking
|
||||
// whether an account exists via a different error message.
|
||||
if ($user_id && !empty($temp['locked_until'])) {
|
||||
if (strtotime($temp['locked_until']) > time()) {
|
||||
// Still within the lockout window — reject
|
||||
$retry_at = date('H:i', strtotime($temp['locked_until']));
|
||||
$answer['message'] = "Too many failed attempts. Please try again after {$retry_at}.";
|
||||
exit(json_encode($answer));
|
||||
} else {
|
||||
// Lockout has expired — reset counter so they get a fresh 10 attempts
|
||||
$pdo1->prepare("UPDATE user SET login_attempts = 0, locked_until = NULL WHERE user_id = :id")
|
||||
->execute([':id' => $user_id]);
|
||||
$temp['login_attempts'] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 3–4: Verify password — exit with error on mismatch ──────────────────
|
||||
if (password_verify(trim($data["password"]), $temp["password"])) {
|
||||
|
||||
// ── Reset lockout on successful password verification ─────────────────────
|
||||
if ($user_id) {
|
||||
$pdo1->prepare("UPDATE user SET login_attempts = 0, locked_until = NULL WHERE user_id = :id")
|
||||
->execute([':id' => $user_id]);
|
||||
}
|
||||
|
||||
// ── Step 5a: Fetch full user record ──────────────────────────────────────
|
||||
// 'support' user gets a hardcoded email so it can always log in even without
|
||||
// a registered email address in the DB.
|
||||
@@ -223,15 +246,10 @@ if (password_verify(trim($data["password"]), $temp["password"])) {
|
||||
// ── Step 5f: Licence expiry check ────────────────────────────────────────
|
||||
// $expire is loaded from db_auth.php via session/preset bootstrap.
|
||||
// If the licence expired more than 1 day ago, reject the login.
|
||||
// Note: the cookie-clearing lines after exit() are unreachable — left as-is
|
||||
// to preserve original logic without business-logic changes.
|
||||
if (strtotime("now") > strtotime($expire . " + 1 day")) {
|
||||
session_destroy();
|
||||
$answer["expire"] = "expire";
|
||||
exit(json_encode($answer));
|
||||
setcookie("u", "", time() - 1, "/"); // unreachable — preserved from original
|
||||
setcookie("h1", "", time() - 1, "/");
|
||||
setcookie("h2", "", time() - 1, "/");
|
||||
}
|
||||
|
||||
// ── Step 6: Generate 6-digit TOTP ────────────────────────────────────────
|
||||
@@ -327,8 +345,25 @@ if (password_verify(trim($data["password"]), $temp["password"])) {
|
||||
} else {
|
||||
|
||||
// ── Password mismatch ─────────────────────────────────────────────────────
|
||||
// Clear identifying cookies on failure to prevent cookie-based session reuse.
|
||||
$answer["message"] = "Incorrect Password";
|
||||
// Only increment the counter when the username is valid — wrong usernames
|
||||
// don't count so a typo in your own name doesn't eat your own attempts.
|
||||
if ($user_id) {
|
||||
$attempts = (int)($temp['login_attempts'] ?? 0) + 1;
|
||||
if ($attempts >= 10) {
|
||||
$locked_until = date('Y-m-d H:i:s', strtotime('+30 minutes'));
|
||||
$pdo1->prepare("UPDATE user SET login_attempts = :a, locked_until = :l WHERE user_id = :id")
|
||||
->execute([':a' => $attempts, ':l' => $locked_until, ':id' => $user_id]);
|
||||
$retry_at = date('H:i', strtotime($locked_until));
|
||||
$answer['message'] = "Too many failed attempts. Please try again after {$retry_at}.";
|
||||
} else {
|
||||
$pdo1->prepare("UPDATE user SET login_attempts = :a WHERE user_id = :id")
|
||||
->execute([':a' => $attempts, ':id' => $user_id]);
|
||||
$answer['message'] = "Incorrect Password";
|
||||
}
|
||||
} else {
|
||||
$answer['message'] = "Incorrect Password";
|
||||
}
|
||||
|
||||
setcookie("u", "", time() - 1, "/");
|
||||
setcookie("h1", "", time() - 1, "/");
|
||||
setcookie("h2", "", time() - 1, "/");
|
||||
|
||||
Reference in New Issue
Block a user