web app security fix

This commit is contained in:
Thanakorn S
2026-05-11 13:49:44 +07:00
parent d989e59edc
commit 7a87909392
7 changed files with 107 additions and 21 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 644 KiB

After

Width:  |  Height:  |  Size: 138 KiB

+45
View File
@@ -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);
+5 -3
View File
@@ -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 -1
View File
@@ -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>
+7 -6
View File
@@ -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>
+5 -2
View File
@@ -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';
+44 -9
View File
@@ -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, "/");