Browse Source

Security: remove hardcoded credentials and add auth to POST endpoints

- letter_authority.php: getPdoSafe() now reads DB creds from $cfg/env instead of hardcoded values
- contracts-admin.php: LOA_TOKEN_SECRET, ADMIN_SHARED_SECRET, ADMIN_USER/PASS now loaded from $cfg/env
- add_stage.php + save_stages.php: add HTTP Basic Auth guard (same realm as edit_application.php); also silence display_errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Benjamin Harris 2 weeks ago
parent
commit
7840d0af7a

+ 15 - 4
contracts/add_stage.php

@@ -1,16 +1,27 @@
 <?php
-//error_reporting(E_ERROR | E_PARSE);
 error_reporting(E_ALL);
-ini_set("display_errors", 1);
+ini_set("display_errors", 0);
+ini_set("log_errors", 1);
 
 date_default_timezone_set("Australia/Hobart");
 ini_set("default_charset", "UTF-8");
 mb_internal_encoding("UTF-8");
 
-require_once 'config.php';
-
 $cfg = require __DIR__ . '/config.php';
 
+$_au = $cfg['admin_user'] ?? '';
+$_ap = $cfg['admin_pass'] ?? '';
+if ($_au === '' || $_ap === '' ||
+    !isset($_SERVER['PHP_AUTH_USER']) ||
+    $_SERVER['PHP_AUTH_USER'] !== $_au ||
+    ($_SERVER['PHP_AUTH_PW'] ?? '') !== $_ap) {
+    header('WWW-Authenticate: Basic realm="Modulos Contracts Admin"');
+    header('HTTP/1.0 401 Unauthorized');
+    echo 'Authentication required.';
+    exit;
+}
+unset($_au, $_ap);
+
 use PHPMailer\PHPMailer\PHPMailer;
 use PHPMailer\PHPMailer\Exception;
 

+ 4 - 4
contracts/contracts-admin/contracts-admin.php

@@ -64,12 +64,12 @@ if (!defined('CONTRACTS_DIR')) {
 
 // Shared secret so the public signing page can mark a contract as "signed"
 if (!defined('ADMIN_SHARED_SECRET')) {
-    define('ADMIN_SHARED_SECRET', 'change-this-secret');
+    define('ADMIN_SHARED_SECRET', $cfg['admin_shared_secret'] ?? getenv('CONTRACTS_ADMIN_SHARED_SECRET') ?: '');
 }
 
 // Admin auth. Use HTTP Basic Auth for the MVP.
-if (!defined('ADMIN_USER')) define('ADMIN_USER', 'admin');
-if (!defined('ADMIN_PASS')) define('ADMIN_PASS', 'changeme');
+if (!defined('ADMIN_USER')) define('ADMIN_USER', $cfg['admin_user'] ?? getenv('CONTRACTS_ADMIN_USER') ?: 'admin');
+if (!defined('ADMIN_PASS')) define('ADMIN_PASS', $cfg['admin_pass'] ?? getenv('CONTRACTS_ADMIN_PASS') ?: '');
 
 // Database configuration. If nothing is defined, we auto-fallback to SQLite.
 if (!defined('DB_DSN'))  define('DB_DSN', 'sqlite:' . __DIR__ . '/contracts.sqlite');
@@ -85,7 +85,7 @@ if (!defined('MAIL_FROM_NAME')) define('MAIL_FROM_NAME', 'Modulos Design');
 if (!defined('LOA_DIR'))         define('LOA_DIR', '../loa'); // sibling to ../contracts
 if (!defined('LOA_BASE_URL'))    define('LOA_BASE_URL', 'https://modulosdesign.com.au/contracts'); // where loa.php lives
 // IMPORTANT: set this to the SAME secret used in loa.php ($CFG['secret'] / APP_HMAC_SECRET)
-if (!defined('LOA_TOKEN_SECRET')) define('LOA_TOKEN_SECRET', 'd1Epy6ryzgLYjLEBlpiHFrgST8JbAjgksjj3hIO5zCK5DChqYpWUdr8jeWR7xEgd');
+if (!defined('LOA_TOKEN_SECRET')) define('LOA_TOKEN_SECRET', $cfg['loa_secret'] ?? getenv('LOA_TOKEN_SECRET') ?: '');
 
 
 $tab = $_GET['tab'] ?? 'contracts';

+ 7 - 6
contracts/letter_authority.php

@@ -86,12 +86,13 @@ function normalizeDrg(string $in): array {
     return [$int, $variants]; // return "3043" as canonical Job #, with variants to query
 }
 
-function getPdoSafe(): PDO {
-    $dsn = "mysql:host=localhost;dbname=client_jobs;charset=utf8mb4";
-    $username = "modulosdesign";
-    $password = "RiznS5DzNgUMXnp";
+function getPdoSafe(array $cfg = []): PDO {
+    $host = $cfg['db_host'] ?? getenv('DB_HOST') ?: 'localhost';
+    $name = $cfg['db_name'] ?? getenv('DB_NAME') ?: 'client_jobs';
+    $user = $cfg['db_username'] ?? getenv('DB_USER') ?: '';
+    $pass = $cfg['db_password'] ?? getenv('DB_PASS') ?: '';
 
-    $pdo = new PDO($dsn, $username, $password, [
+    $pdo = new PDO("mysql:host=$host;dbname=$name;charset=utf8mb4", $user, $pass, [
         PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
         PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
         PDO::ATTR_EMULATE_PREPARES   => false,
@@ -765,7 +766,7 @@ function sendSignedLoaEmails(array $cfg, string $fromAddress, array $row, string
 // ---------------------------------------------------------------------
 // MAIN FLOW
 // ---------------------------------------------------------------------
-$pdo = getPdoSafe();
+$pdo = getPdoSafe($cfg);
 
 // $clientId = isset($_GET['clientid']) && preg_match('/^\d{1,10}$/', $_GET['clientid']) ? $_GET['clientid'] : 'unknown';
 $drgRaw = $_GET['drg'] ?? $_GET['clientid'] ?? '';

+ 15 - 1
contracts/save_stages.php

@@ -1,11 +1,25 @@
 <?php
 error_reporting(E_ALL);
-ini_set("display_errors", 1);
+ini_set("display_errors", 0);
+ini_set("log_errors", 1);
 
 date_default_timezone_set("Australia/Hobart");
 
 $cfg = require __DIR__ . '/config.php';
 
+$_au = $cfg['admin_user'] ?? '';
+$_ap = $cfg['admin_pass'] ?? '';
+if ($_au === '' || $_ap === '' ||
+    !isset($_SERVER['PHP_AUTH_USER']) ||
+    $_SERVER['PHP_AUTH_USER'] !== $_au ||
+    ($_SERVER['PHP_AUTH_PW'] ?? '') !== $_ap) {
+    header('WWW-Authenticate: Basic realm="Modulos Contracts Admin"');
+    header('HTTP/1.0 401 Unauthorized');
+    echo 'Authentication required.';
+    exit;
+}
+unset($_au, $_ap);
+
 $dsn = 'mysql:host=' . $cfg['db_host'] . ';dbname=' . $cfg['db_name'] . ';charset=utf8mb4';
 $options = [
     PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,