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 from_wh = data.ref.ref_warehouse;
|
||||||
var to_wh = data.ref_warehouse;
|
var to_wh = data.ref_warehouse;
|
||||||
|
|
||||||
// Lock lot/serial
|
// Lock lot/serial — use DOM construction (not .html()) to prevent XSS
|
||||||
$('#lot_number').html('<option value="' + (data.lot_number || '') + '">' + (data.lot_number || '—') + '</option>')
|
$('#lot_number').empty()
|
||||||
|
.append($('<option>').val(data.lot_number || '').text(data.lot_number || '—'))
|
||||||
.val(data.lot_number || '').prop('disabled', true);
|
.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);
|
.val(data.serial_number || '').prop('disabled', true);
|
||||||
|
|
||||||
$('#product_name').val(data.product_name);
|
$('#product_name').val(data.product_name);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<footer class="text-center py-2 mt-6 text-secondary ">
|
<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>
|
target="_blank" class="text-primary">TR3</a> </p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
// Security headers — emitted before any HTML output.
|
// Security headers — emitted before any HTML output.
|
||||||
// X-Content-Type-Options: prevents MIME-sniffing attacks.
|
|
||||||
header('X-Content-Type-Options: nosniff');
|
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');
|
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');
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -12,7 +9,7 @@ header('Referrer-Policy: strict-origin-when-cross-origin');
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>TRx WMS</title>
|
<title>MN3 WMS</title>
|
||||||
<meta name="csrf-token" content="<?= htmlspecialchars($_SESSION['csrf_token'] ?? '') ?>">
|
<meta name="csrf-token" content="<?= htmlspecialchars($_SESSION['csrf_token'] ?? '') ?>">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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">
|
<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 -->
|
<!-- jquery -->
|
||||||
<script src="https://code.jquery.com/jquery-3.7.1.js" integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4=" crossorigin="anonymous"></script>
|
<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 -->
|
<!-- 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>
|
<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 -->
|
<!-- bootbox -->
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.4.0/bootbox.min.js"></script>
|
<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 -->
|
<!-- theme script -->
|
||||||
<link rel="stylesheet" href="<?php echo $server_url?>assets/css/main.css">
|
<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>
|
<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">
|
<link rel="stylesheet" href="<?php echo $server_url?>assets/css/custom.css">
|
||||||
<script src="<?php echo $server_url?>assets/js/custom.js"></script>
|
<script src="<?php echo $server_url?>assets/js/custom.js"></script>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
// logout once session expire
|
// Redirect to login if the user has not completed full authentication.
|
||||||
if(!isset($_SESSION["otp"])){
|
// 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');
|
header('Location: '.$server_url.'login/index.php');
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
require 'preset.php';
|
require 'preset.php';
|
||||||
|
|||||||
@@ -70,14 +70,37 @@ $user_id = $sth->fetchColumn();
|
|||||||
|
|
||||||
$username = strtolower($data["username"]);
|
$username = strtolower($data["username"]);
|
||||||
|
|
||||||
// ── Step 2: Fetch the user's hashed password ──────────────────────────────────
|
// ── Step 2: Fetch the user's hashed password + lockout state ─────────────────
|
||||||
$sth = $pdo1->prepare("select password from user where username = ? or email = ? limit 1;");
|
$sth = $pdo1->prepare("SELECT password, login_attempts, locked_until FROM user WHERE username = ? OR email = ? LIMIT 1;");
|
||||||
$sth->execute(array($username, $username));
|
$sth->execute(array($username, $username));
|
||||||
$temp = $sth->fetch(PDO::FETCH_ASSOC);
|
$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 ──────────────────
|
// ── Step 3–4: Verify password — exit with error on mismatch ──────────────────
|
||||||
if (password_verify(trim($data["password"]), $temp["password"])) {
|
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 ──────────────────────────────────────
|
// ── Step 5a: Fetch full user record ──────────────────────────────────────
|
||||||
// 'support' user gets a hardcoded email so it can always log in even without
|
// 'support' user gets a hardcoded email so it can always log in even without
|
||||||
// a registered email address in the DB.
|
// a registered email address in the DB.
|
||||||
@@ -223,15 +246,10 @@ if (password_verify(trim($data["password"]), $temp["password"])) {
|
|||||||
// ── Step 5f: Licence expiry check ────────────────────────────────────────
|
// ── Step 5f: Licence expiry check ────────────────────────────────────────
|
||||||
// $expire is loaded from db_auth.php via session/preset bootstrap.
|
// $expire is loaded from db_auth.php via session/preset bootstrap.
|
||||||
// If the licence expired more than 1 day ago, reject the login.
|
// 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")) {
|
if (strtotime("now") > strtotime($expire . " + 1 day")) {
|
||||||
session_destroy();
|
session_destroy();
|
||||||
$answer["expire"] = "expire";
|
$answer["expire"] = "expire";
|
||||||
exit(json_encode($answer));
|
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 ────────────────────────────────────────
|
// ── Step 6: Generate 6-digit TOTP ────────────────────────────────────────
|
||||||
@@ -327,8 +345,25 @@ if (password_verify(trim($data["password"]), $temp["password"])) {
|
|||||||
} else {
|
} else {
|
||||||
|
|
||||||
// ── Password mismatch ─────────────────────────────────────────────────────
|
// ── Password mismatch ─────────────────────────────────────────────────────
|
||||||
// Clear identifying cookies on failure to prevent cookie-based session reuse.
|
// Only increment the counter when the username is valid — wrong usernames
|
||||||
$answer["message"] = "Incorrect Password";
|
// 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("u", "", time() - 1, "/");
|
||||||
setcookie("h1", "", time() - 1, "/");
|
setcookie("h1", "", time() - 1, "/");
|
||||||
setcookie("h2", "", time() - 1, "/");
|
setcookie("h2", "", time() - 1, "/");
|
||||||
|
|||||||
Reference in New Issue
Block a user