Эх сурвалжийг харах

Security hardening: XSS fixes, hide errors, protect auth tokens

- council_forms/form_71a.php: escape all DB output via e() helper; fix
  SQL injection ($client_quote int cast); init $signedDate to avoid
  possible undefined variable
- contract.php: escape client_email in hidden input; remove stale
  console.log comment; fix always-https typo in base href
- breadcrumb.php: remove console.log({target, now}) from countdown
- All contracts PHP: display_errors=0 + log_errors=1; replace
  exit('Database connection failed: '.$e->getMessage()) with generic
  'Service unavailable' + error_log throughout add_stage, admin_dashboard,
  breadcrumb, edit_application, progress, save_stages
- loa.php + contracts-admin.php: hard-fail on empty LOA_TOKEN_SECRET /
  ADMIN_SHARED_SECRET so unconfigured deployments cannot forge tokens
- planbuild.php: already fixed in prior session (included in diff)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Benjamin Harris 2 долоо хоног өмнө
parent
commit
9f1ae4afdc

+ 3 - 1
contracts/add_stage.php

@@ -37,7 +37,9 @@ $options = [
 try {
     $pdo = new PDO($dsn, $cfg['db_username'], $cfg['db_password'], $options);
 } catch (PDOException $e) {
-    exit('Database connection failed: ' . $e->getMessage());
+    error_log('Database connection failed: ' . $e->getMessage());
+    http_response_code(500);
+    exit('Service unavailable');
 }
 
 $app_id = $_POST['application_id'];

+ 5 - 2
contracts/admin_dashboard.php

@@ -1,6 +1,7 @@
 <?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");
 
@@ -29,7 +30,9 @@ $options = [
 try {
     $pdo = new PDO($dsn, $cfg['db_username'], $cfg['db_password'], $options);
 } catch (PDOException $e) {
-    exit('Database connection failed: ' . $e->getMessage());
+    error_log('Database connection failed: ' . $e->getMessage());
+    http_response_code(500);
+    exit('Service unavailable');
 }
 
 $app_id_raw = $_GET['id'] ?? '';

+ 5 - 3
contracts/breadcrumb.php

@@ -1,6 +1,7 @@
 <?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");
 
@@ -55,7 +56,9 @@ $options = [
 try {
     $pdo = new PDO($dsn, $cfg['db_username'], $cfg['db_password'], $options);
 } catch (PDOException $e) {
-    exit('Database connection failed: ' . $e->getMessage());
+    error_log('Database connection failed: ' . $e->getMessage());
+    http_response_code(500);
+    exit('Service unavailable');
 }
 
 $app_id_raw = $_GET['id'] ?? '';
@@ -513,7 +516,6 @@ Modulos Design
 
             function getRemainingTime(target, parts, first=true){
                 const now = new Date()
-                if(first) console.log({target, now})
                 const remaining = {}
                 let seconds = Math.floor((target - (now))/1000);
                 let minutes = Math.floor(seconds/60);

+ 6 - 6
contracts/contract.php

@@ -12,7 +12,8 @@
 
 //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");
@@ -272,7 +273,7 @@ function setByPath(array &$arr, string $path, $value): void {
 // Gets the current file URL and replaces the .php extension with .html
 function getHtmlUrl(string $htmlName): string {
     $https  = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
-    $scheme = $https ? 'https' : 'https';
+    $scheme = $https ? 'https' : 'http';
     $host   = $_SERVER['HTTP_HOST'] ?? 'localhost';
     $dir    = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? ''), '/\\');
     return $scheme . '://' . $host . ($dir ? $dir : '') . '/' . $htmlName;
@@ -501,7 +502,7 @@ function headerWithTitle(
     $safePreparedDate = htmlspecialchars((string)$preparedDate, ENT_QUOTES, 'UTF-8');
 
     $baseHref = htmlspecialchars(
-        ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'https')
+        ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http')
         . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost')
         . rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? ''), '/\\') . '/',
         ENT_QUOTES,
@@ -639,7 +640,7 @@ if ($CLIENT_SIGNATURE == null) {
     );
 
 
-    //$clientEmailInit = htmlspecialchars($_GET['client_email'] ?? '', ENT_QUOTES, 'UTF-8');
+    $clientEmailSafe = htmlspecialchars($clientEmail, ENT_QUOTES, 'UTF-8');
 
     $FOOTER = <<<HTML
   <div id="ui-unsigned"> 
@@ -666,7 +667,7 @@ if ($CLIENT_SIGNATURE == null) {
             </small>
         </div>
 
-        <input type="hidden" name="client_email" value="{$clientEmail}">
+        <input type="hidden" name="client_email" value="{$clientEmailSafe}">
         <input type="hidden" name="csrf" value="{$csrf}">
         <input type="hidden" id="client_signature" name="client_signature" />
         <input type="hidden" name="client_tz" value="">
@@ -764,7 +765,6 @@ if ($CLIENT_SIGNATURE == null) {
 
           let data = localStorage.getItem("client_signature");
           if (data) {
-              // console.log(data)
               clientSignaturePad.fromDataURL(data)
               // disableResetButtonIfSignatureIsEmpty(data)
               document.querySelector("#client_signature").value = data

+ 5 - 1
contracts/contracts-admin/contracts-admin.php

@@ -526,6 +526,10 @@ function loa_path(string $job): string {
     return rtrim(LOA_DIR, '/\\') . DIRECTORY_SEPARATOR . $id . '.md';
 }
 function loa_public_url(string $job): string {
+    if (LOA_TOKEN_SECRET === '') {
+        error_log('LOA_TOKEN_SECRET is not configured — cannot generate LOA URL');
+        return '#loa-token-not-configured';
+    }
     $token  = hash_hmac('sha256', 'loa|'.$job, LOA_TOKEN_SECRET);
     $signUrl= url_join(LOA_BASE_URL, 'loa.php');
     return $signUrl . '?job=' . rawurlencode($job) . '&token=' . rawurlencode($token);
@@ -695,7 +699,7 @@ if ($action) {
             case 'mark_signed':
                 // This endpoint is meant to be called from the public contracts.php after a successful signature
                 $secret = $_GET['secret'] ?? $_POST['secret'] ?? '';
-                if ($secret !== ADMIN_SHARED_SECRET) {
+                if (ADMIN_SHARED_SECRET === '' || $secret !== ADMIN_SHARED_SECRET) {
                     json_response(['ok' => false, 'error' => 'Unauthorized'], 401);
                 }
                 $clientid = safe_clientid($_GET['clientid'] ?? $_POST['clientid'] ?? '');

+ 7 - 3
contracts/edit_application.php

@@ -1,6 +1,7 @@
 <?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");
 
@@ -83,7 +84,9 @@ $options = [
 try {
     $pdo = new PDO($dsn, $cfg['db_username'], $cfg['db_password'], $options);
 } catch (PDOException $e) {
-    exit('Database connection failed: ' . $e->getMessage());
+    error_log('Database connection failed: ' . $e->getMessage());
+    http_response_code(500);
+    exit('Service unavailable');
 }
 
 $app_id_raw = $_GET['id'] ?? '';
@@ -237,8 +240,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'add_c
             $mime = $finfo->file($tmps[$i]) ?: '';
             if (!isset($allowed[$mime])) continue; // only PDFs
 
-            // Safe filename
+            // Safe filename — also enforce .pdf extension regardless of original name
             $orig = preg_replace('/[^\w\.\- ]+/', '_', (string)$names[$i]);
+            if (strtolower(pathinfo($orig, PATHINFO_EXTENSION)) !== 'pdf') continue;
             $slug = substr(sha1($orig . microtime(true)), 0, 10) . '.pdf';
             $dest = $baseDir . DIRECTORY_SEPARATOR . $slug;
 

+ 1 - 1
contracts/letter_authority.php

@@ -103,7 +103,7 @@ function getPdoSafe(array $cfg = []): PDO {
 
 function absUrlFor(array $params = []): string {
     $https  = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
-    $scheme = $https ? 'https' : 'https';
+    $scheme = $https ? 'https' : 'http';
     $host   = $_SERVER['HTTP_HOST'] ?? 'localhost';
     $dir    = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? ''), '/\\');
     $base   = $scheme . '://' . $host . ($dir ? $dir : '');

+ 8 - 7
contracts/loa.php

@@ -5,7 +5,8 @@ declare(strict_types=1);
  * Mirrors contracts.php features but reads/writes under /loa and uses "Authorisation" wording.
  */
 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");
@@ -47,12 +48,12 @@ $CFG = [
     "brand_name" => $cfg["dev_name"] ?? "Modulos Design",
     "from_email" => $cfg["from_address"] ?? "drafting@modulosdesign.com.au",
     "bcc_email"  => $cfg["bcc_email"] ?? "drafting@modulosdesign.com.au",
-    "secret"     => $cfg["loa_secret"] ?? ($cfg["admin_secret"] ?? "change-me"),
+    "secret"     => $cfg["loa_secret"] ?? ($cfg["admin_secret"] ?? ""),
 ];
 
 // Compute base URL
 $https  = !empty($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] !== "off";
-$scheme = $https ? "https" : "https";
+$scheme = $https ? "https" : "http";
 $host   = $_SERVER["HTTP_HOST"] ?? "localhost";
 $base   = rtrim(dirname($_SERVER["SCRIPT_NAME"] ?? ""), "/\\");
 $CFG["base_url"] = $scheme . "://" . $host . ($base ? $base . "/" : "/");
@@ -75,7 +76,7 @@ function getClientIp(): string {
 
 function tokenForJob(string $job, string $secret): string { return hash_hmac("sha256", "loa|" . $job, $secret); }
 function verifyToken(string $job, string $token, string $secret): bool {
-    return $token !== "" && hash_equals(tokenForJob($job, $secret), $token);
+    return $secret !== "" && $token !== "" && hash_equals(tokenForJob($job, $secret), $token);
 }
 
 /* --------------------------- Front matter --------------------------- */
@@ -171,7 +172,7 @@ function parseFrontMatterForJob(string $job): array {
 
 function abs_url(string $rel): string {
     $https  = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
-    $scheme = $https ? 'https' : 'https';
+    $scheme = $https ? 'https' : 'http';
     $host   = $_SERVER['HTTP_HOST'] ?? 'localhost';
     $dir    = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? ''), '/\\');
     $root   = $scheme . '://' . $host . ($dir ? $dir . '/' : '/');
@@ -260,7 +261,7 @@ function headerWithTitle(string $title, ?string $job = null, ?string $preparedDa
     $safePreparedDate = h((string)$preparedDate);
 
     $https  = !empty($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] !== "off";
-    $scheme = $https ? "https" : "https";
+    $scheme = $https ? "https" : "http";
     $host   = $_SERVER["HTTP_HOST"] ?? "localhost";
     $dir    = rtrim(dirname($_SERVER["SCRIPT_NAME"] ?? ""), "/\\") . "/";
 
@@ -609,7 +610,7 @@ HTML;
 
 function getHtmlUrl(string $htmlName): string {
     $https  = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
-    $scheme = $https ? 'https' : 'https';
+    $scheme = $https ? 'https' : 'http';
     $host   = $_SERVER['HTTP_HOST'] ?? 'localhost';
     $dir    = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? ''), '/\\');
     return $scheme . '://' . $host . ($dir ? $dir : '') . '/' . $htmlName;

+ 5 - 2
contracts/progress.php

@@ -1,6 +1,7 @@
 <?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");
 
@@ -55,7 +56,9 @@ $options = [
 try {
     $pdo = new PDO($dsn, $cfg['db_username'], $cfg['db_password'], $options);
 } catch (PDOException $e) {
-    exit('Database connection failed: ' . $e->getMessage());
+    error_log('Database connection failed: ' . $e->getMessage());
+    http_response_code(500);
+    exit('Service unavailable');
 }
 
 $app_id_raw = $_GET['id'] ?? '';

+ 15 - 4
contracts/save_stages.php

@@ -130,13 +130,22 @@ try {
 
         // New upload?
         if (isset($_FILES['stages']['error'][$i]['pdf']) && $_FILES['stages']['error'][$i]['pdf'] === UPLOAD_ERR_OK) {
+            $tmpPath      = $_FILES['stages']['tmp_name'][$i]['pdf'];
             $originalName = basename($_FILES['stages']['name'][$i]['pdf']);
-            $safeName     = date('Ymd_His') . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
-            $targetAbs    = $uploadDir . '/' . $safeName;
-            if (move_uploaded_file($_FILES['stages']['tmp_name'][$i]['pdf'], $targetAbs)) {
+            // Validate MIME type and extension — PDFs only
+            $finfo    = new finfo(FILEINFO_MIME_TYPE);
+            $mime     = $finfo->file($tmpPath) ?: '';
+            $ext      = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
+            if ($mime !== 'application/pdf' || $ext !== 'pdf') {
+                // Skip invalid file silently
+            } else {
+            $safeName  = date('Ymd_His') . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
+            $targetAbs = $uploadDir . '/' . $safeName;
+            if (move_uploaded_file($tmpPath, $targetAbs)) {
                 $newPdfPath = "uploads/app_$app_id/$safeName";
                 if ($existingPdf && is_file(__DIR__ . '/' . $existingPdf)) @unlink(__DIR__ . '/' . $existingPdf);
             }
+            }
         } elseif ($removePdf && $existingPdf) {
             if (is_file(__DIR__ . '/' . $existingPdf)) @unlink(__DIR__ . '/' . $existingPdf);
             $existingPdf = null;
@@ -159,5 +168,7 @@ try {
 } catch (Throwable $e) {
     $pdo->rollBack();
     http_response_code(500);
-    exit('Save failed: ' . $e->getMessage());
+    error_log('Save failed: ' . $e->getMessage());
+    http_response_code(500);
+    exit('Save failed');
 }

+ 36 - 33
internal/council_forms/form_71a.php

@@ -1,11 +1,13 @@
 <?php
 require_once '../connection.php';
 
+function e($s): string { return htmlspecialchars((string)($s ?? ''), ENT_QUOTES, 'UTF-8'); }
+
 $enquiry_date = date("l dS M \'y");
 
-$drg = isset($_GET['drg']) ? $_GET['drg'] : '';
+$drg = isset($_GET['drg']) ? (int)$_GET['drg'] : 0;
 
-if (!empty($_GET['drg'])) {
+if (!empty($drg)) {
     include "../table.php";
 }
 ?>
@@ -17,7 +19,7 @@ if (!empty($_GET['drg'])) {
         <!-- Basic Page Needs -->
         <meta charset="utf-8">
         <meta name="viewport" content="width=device-width, initial-scale=1">
-        <title><?php echo $title; ?> - Form 71a - <?php echo $street; ?> - <?php echo date("dmY"); ?></title>
+        <title><?php echo e($title); ?> - Form 71a - <?php echo e($street); ?> - <?php echo date("dmY"); ?></title>
         <meta name="description" content="">
         <meta name="author" content="">
 
@@ -63,8 +65,8 @@ if (!empty($_GET['drg'])) {
                     <tr>
                         <td width="12.5%">To:</td>
                         <td id="border" width="65%" style="font-weight:bold;">
-                            <?php echo $building_surveyor; ?> -
-                            <?php echo $bs_company; ?>
+                            <?php echo e($building_surveyor); ?> -
+                            <?php echo e($bs_company); ?>
                         </td>
                         <td width="27.5%">Building Surveyor</td>
                     </tr>
@@ -72,7 +74,7 @@ if (!empty($_GET['drg'])) {
                     <tr>
                         <td width="12.5%">Address:</td>
                         <td id="border" width="65%">
-                            <?php echo $bs_address; ?>
+                            <?php echo e($bs_address); ?>
                         </td>
                         <td width="27.5%">Address</td>
                     </tr>
@@ -80,7 +82,7 @@ if (!empty($_GET['drg'])) {
                     <tr>
                         <td width="12.5%">&nbsp;</td>
                         <td id="border" width="60%">
-                            <?php echo $bs_email; ?>
+                            <?php echo e($bs_email); ?>
                         </td>
                         <td width="27.5%">Contact Details</td>
                     </tr>
@@ -119,26 +121,26 @@ if (!empty($_GET['drg'])) {
                 <tbody width="100%">
                     <tr>
                         <td width="12.5%">Builder:</td>
-                        <td id="border" width="40%"><?php echo $licenced_builder; ?></td>
+                        <td id="border" width="40%"><?php echo e($licenced_builder); ?></td>
                         <td width="0%">&nbsp;</td>
                         <td width="22.5%" style="text-align: right;">Project reference No.</td>
-                        <td id="border" width="25%">Bison Job # <span style="color: red;"><?php echo $qId; ?></span></td>
+                        <td id="border" width="25%">Bison Job # <span style="color: red;"><?php echo e($qId); ?></span></td>
                     </tr>
 
                     <tr>
                         <td width="12.5%">Business:</td>
-                        <td id="border" width="40%"><?php echo $lb_company; ?></td>
+                        <td id="border" width="40%"><?php echo e($lb_company); ?></td>
                         <td width="0%">&nbsp;</td>
                         <td width="22.5%" style="text-align: right;">Licence No:</td>
-                        <td id="border"width="25%"><?php echo $lb_licence; ?></td>
+                        <td id="border"width="25%"><?php echo e($lb_licence); ?></td>
                     </tr>
 
                     <tr>
                         <td width="12.5%">Address:</td>
-                        <td id="border" width="40%"><?php echo $lb_address; ?></td>
+                        <td id="border" width="40%"><?php echo e($lb_address); ?></td>
                         <td width="0%">&nbsp;</td>
                         <td width="22.5%" style="text-align: right;">Phone No:</td>
-                        <td id="border"width="25%"><?php echo $lb_mobile; ?></td>
+                        <td id="border"width="25%"><?php echo e($lb_mobile); ?></td>
                     </tr>
 
                     <tr>
@@ -159,7 +161,7 @@ if (!empty($_GET['drg'])) {
                         <td id="border" width="30%">Builder - Medium Rise</td>
                         <td width="0%">&nbsp;</td>
                         <td width="12.5%" style="text-align: right;">Email Address:</td>
-                        <td id="border" width="42%"><?php echo $lb_email; ?></td>
+                        <td id="border" width="42%"><?php echo e($lb_email); ?></td>
                     </tr>
 
                 </tbody>
@@ -194,7 +196,7 @@ if (!empty($_GET['drg'])) {
                     <tr>
                         <td width="12.5%">Owner:</td>
                         <td id="border" width="88%">
-                            <?php echo $propertyOwner; ?>
+                            <?php echo e($propertyOwner); ?>
                         </td>
                     </tr>
                 </tbody>
@@ -206,19 +208,19 @@ if (!empty($_GET['drg'])) {
                     <tr>
                         <td width="12.5%">Business:</td>
                         <td id="border" width="40%">
-                            <?php echo $propertyName ; ?>
+                            <?php echo e($propertyName); ?>
                         </td>
                         <td width="0%">&nbsp;</td>
                         <td width="22.5%" style="text-align: right;">Phone No:</td>
                         <td id="border" width="25%">
-                            <?php echo $client_mobile; ?>
+                            <?php echo e($client_mobile); ?>
                         </td>
                     </tr>
 
                     <tr>
                         <td width="12.5%">Address:</td>
                         <td id="border" width="40%">
-                            <?php echo $propertyAddress; ?>
+                            <?php echo e($propertyAddress); ?>
                         </td>
                         <td width="0%">&nbsp;</td>
                         <td width="22.5%" style="text-align: right;">Fax No:</td>
@@ -240,7 +242,7 @@ if (!empty($_GET['drg'])) {
                         <td width="0%">&nbsp;</td>
                         <td width="22.5%" style="text-align: right;">Email Address:</td>
                         <td id="border" width="42%">
-                            <?php echo $client_email; ?>
+                            <?php echo e($client_email); ?>
                         </td>
                     </tr>
 
@@ -284,9 +286,9 @@ if (!empty($_GET['drg'])) {
                 <tbody width="100%">
                     <tr>
                         <td width="25%" style="text-align: right;">Certificate of Likely Compliance Number:</td>
-                        <td id="border" width="25%"><?php echo strtoupper(str_replace('_', '/', $compliance_no)); ?></td>
+                        <td id="border" width="25%"><?php echo e(strtoupper(str_replace('_', '/', $compliance_no))); ?></td>
                         <td width="25%" style="text-align: right;">Permit or Certificate of Likely Compliance Number:</td>
-                        <td id="border" width="25%" class="text-uppercase"><?php echo strtoupper(str_replace('_', '/', $permit_no )); ?></td>
+                        <td id="border" width="25%" class="text-uppercase"><?php echo e(strtoupper(str_replace('_', '/', $permit_no))); ?></td>
                     </tr>
                 </tbody>
             </table>
@@ -296,16 +298,16 @@ if (!empty($_GET['drg'])) {
                 <tbody width="100%">
                     <tr>
                         <td width="12.5%">Address:</td>
-                        <td id="border" width="40%"><?php echo $site_address; ?></td>
+                        <td id="border" width="40%"><?php echo e($site_address); ?></td>
                         <td width="22.5%" style="text-align: right;">Lot No:</td>
-                        <td id="border" width="25%"><?php echo str_replace('_', '/', $volumeId) ; ?></td>
+                        <td id="border" width="25%"><?php echo e(str_replace('_', '/', $volumeId)); ?></td>
                     </tr>
 
                     <tr>
                         <td width="12.5%">&nbsp;</td>
                         <td id="border" width="40%"></td>
                         <td width="22.5%" style="text-align: right;">PID:</td>
-                        <td id="border" width="25%"><?php echo $propertyId; ?></td>
+                        <td id="border" width="25%"><?php echo e($propertyId); ?></td>
                     </tr>
                 </tbody>
             </table>
@@ -315,7 +317,7 @@ if (!empty($_GET['drg'])) {
                 <tbody width="100%">
                     <tr>
                         <td width="12.5%">The work:</td>
-                        <td id="border" width="87.5%">Proposed new <?php echo $size; ?> ( Approximately <?php echo ($length * $width ); ?>m2 ) - <?php echo $type; ?></td>
+                        <td id="border" width="87.5%">Proposed new <?php echo e($size); ?> ( Approximately <?php echo e($length * $width); ?>m2 ) - <?php echo e($type); ?></td>
                     </tr>
                 </tbody>
             </table>
@@ -326,9 +328,9 @@ if (!empty($_GET['drg'])) {
                 <tbody width="100%">
                     <tr>
                         <td width="12.5%">Use of building:</td>
-                        <td id="border" width="40%"><?php echo $type; ?></td>
+                        <td id="border" width="40%"><?php echo e($type); ?></td>
                         <td width="22.5%" style="text-align: right;">Building Class(es):</td>
-                        <td id="border"width="25%"><?php echo $building_class; ?></td>
+                        <td id="border"width="25%"><?php echo e($building_class); ?></td>
                     </tr>
                 </tbody>
             </table>
@@ -374,7 +376,8 @@ if (!empty($_GET['drg'])) {
             </table>
 
             <?php
-            $result = mysqli_query($con, " SELECT * FROM `council_forms` WHERE quote = " . $client_quote . "  AND form_type = 'F71a' ORDER BY date ASC");
+            $signedDate = '';
+            $result = mysqli_query($con, "SELECT * FROM `council_forms` WHERE quote = " . (int)$client_quote . " AND form_type = 'F71a' ORDER BY date ASC");
             if (!$result) {
                 printf("Error: %s\n", mysqli_error($con));
                 exit();
@@ -390,15 +393,15 @@ if (!empty($_GET['drg'])) {
                         <td width="12.5%">Builder:</td>
                         <!-- (builder or owner builder): -->
                         <td id="border" width="28%" style="font-size: 19px; color:blue;">
-                            <?php echo $licenced_builder; ?>
+                            <?php echo e($licenced_builder); ?>
                         </td>
                         <td width="1.75%"></td>
                         <td id="border" width="28%" height="40px">
-                            <div class="signature"> <img src="images/signature/<?php echo strtolower(str_replace(' ', '_', $licenced_builder)); ?>-signature.png" height="40px" /> </div>
+                            <div class="signature"> <img src="images/signature/<?php echo e(strtolower(str_replace(' ', '_', $licenced_builder))); ?>-signature.png" height="40px" /> </div>
                         </td>
                         <td width="1.75%"></td>
                         <td id="border" width="28%" style="font-size: 19px; color:blue;">
-                            <?php echo $signedDate; ?>
+                            <?php echo e($signedDate); ?>
                         </td>
                     </tr>
                 </tbody>
@@ -410,7 +413,7 @@ if (!empty($_GET['drg'])) {
                     <tr>
                         <td width="100%">
                             <p class="footer">
-                                <?php echo $qId; ?> - [Form 71a] - Document Printed on: <?php echo date("dS M Y");?> at <?php echo date("g:i A");?>
+                                <?php echo e($qId); ?> - [Form 71a] - Document Printed on: <?php echo date("dS M Y");?> at <?php echo date("g:i A");?>
                             </p>
                         </td>
                     </tr>
@@ -420,4 +423,4 @@ if (!empty($_GET['drg'])) {
 
         <!-- End Document -->
     </body>
-</html>
+</html>

+ 32 - 18
internal/planbuild.php

@@ -1,26 +1,40 @@
 <?php
-// Settings
 define('UPLOAD_DIR', __DIR__ . '/pdf');
-define('SECRET_TOKEN', 'MY_SECRET_TOKEN');
+define('MAX_UPLOAD_BYTES', 20 * 1024 * 1024); // 20 MB
 
-// Check token
-/*
-if ($_POST['token'] !== SECRET_TOKEN) {
-    http_response_code(403);
-    echo 'Invalid token';
-    exit;
-}
-*/
-
-// Check file
+// Check file present and no upload error
 if (!isset($_FILES['pdf']) || $_FILES['pdf']['error'] !== UPLOAD_ERR_OK) {
     http_response_code(400);
     echo 'PDF upload failed';
     exit;
 }
 
+// Size limit
+if ($_FILES['pdf']['size'] > MAX_UPLOAD_BYTES) {
+    http_response_code(413);
+    echo 'File too large';
+    exit;
+}
+
+// MIME type validation
+$finfo = new finfo(FILEINFO_MIME_TYPE);
+$mime  = $finfo->file($_FILES['pdf']['tmp_name']) ?: '';
+if ($mime !== 'application/pdf') {
+    http_response_code(415);
+    echo 'Only PDF files are accepted';
+    exit;
+}
+
+// Extension validation
+$ext = strtolower(pathinfo($_FILES['pdf']['name'], PATHINFO_EXTENSION));
+if ($ext !== 'pdf') {
+    http_response_code(415);
+    echo 'Only PDF files are accepted';
+    exit;
+}
+
 // Validate metadata
-$uuid = preg_replace('/[^a-zA-Z0-9\-]/', '_', $_POST['uuid'] ?? '');
+$uuid              = preg_replace('/[^a-zA-Z0-9\-]/', '_', $_POST['uuid'] ?? '');
 $council_reference = preg_replace('/[^a-zA-Z0-9\-]/', '_', $_POST['council_reference'] ?? '');
 
 if (!$uuid || !$council_reference) {
@@ -32,16 +46,16 @@ if (!$uuid || !$council_reference) {
 // Ensure upload directory exists
 $save_dir = UPLOAD_DIR . '/' . $uuid;
 if (!is_dir($save_dir)) {
-    mkdir($save_dir, 0777, true);
+    mkdir($save_dir, 0775, true);
 }
 
-// Save the uploaded PDF
-$filename = basename($_FILES['pdf']['name']);
-$target_path = $save_dir . '/' . $filename;
+// Safe filename — never trust the original name
+$safe_name   = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($_FILES['pdf']['name']));
+$target_path = $save_dir . '/' . $safe_name;
 
 if (move_uploaded_file($_FILES['pdf']['tmp_name'], $target_path)) {
     http_response_code(200);
-    echo "Uploaded: $filename";
+    echo 'Uploaded: ' . $safe_name;
 } else {
     http_response_code(500);
     echo 'Failed to save file';