From 7e523392c04454c3a3131f64114655d2e0e590cd Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Tue, 23 Dec 2025 22:30:39 -0500 Subject: [PATCH] Add backend PIN verification and security hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add requirePin() check on add/update/delete endpoints (closes PIN bypass vulnerability) - Restrict CORS to specific allowed origins only - Add input length limits to sanitize() function - Frontend now sends currentPin with all write requests - Deploy script copies data/index.php to block directory listing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- api.php | 60 ++++++++++++++++++++++++++++++++++++++++++++++-------- deploy | 3 +++ index.html | 13 ++++++++---- 3 files changed, 63 insertions(+), 13 deletions(-) diff --git a/api.php b/api.php index 80720bd..4d4436e 100644 --- a/api.php +++ b/api.php @@ -1,13 +1,22 @@ $maxLength) { + $str = substr($str, 0, $maxLength); + } + return $str; +} + +/** + * Check if PIN is required and valid for write operations + */ +function requirePin($input) { + // If no PIN is set yet, allow writes (first-time setup) + if (!hasPinSet()) { + return true; + } + + // PIN is set, so require it for write operations + $pin = $input['pin'] ?? ''; + return verifyPin($pin); } // Main request handling @@ -195,10 +222,15 @@ switch ($action) { break; case 'add': + if (!requirePin($input)) { + echo json_encode(['success' => false, 'error' => 'Invalid PIN']); + break; + } + $name = sanitize($input['name'] ?? ''); - $container = sanitize($input['container'] ?? 'glass-bail-medium'); + $container = sanitize($input['container'] ?? 'glass-bail-medium', 50); $outOfStock = (bool)($input['outOfStock'] ?? false); - + if (empty($name)) { echo json_encode(['success' => false, 'error' => 'Name required']); break; @@ -219,11 +251,16 @@ switch ($action) { break; case 'update': + if (!requirePin($input)) { + echo json_encode(['success' => false, 'error' => 'Invalid PIN']); + break; + } + $id = (int)($input['id'] ?? 0); $name = sanitize($input['name'] ?? ''); - $container = sanitize($input['container'] ?? ''); + $container = sanitize($input['container'] ?? '', 50); $outOfStock = (bool)($input['outOfStock'] ?? false); - + if ($id <= 0) { echo json_encode(['success' => false, 'error' => 'Invalid ID']); break; @@ -252,8 +289,13 @@ switch ($action) { break; case 'delete': + if (!requirePin($input)) { + echo json_encode(['success' => false, 'error' => 'Invalid PIN']); + break; + } + $id = (int)($input['id'] ?? 0); - + if ($id <= 0) { echo json_encode(['success' => false, 'error' => 'Invalid ID']); break; diff --git a/deploy b/deploy index 3365eba..5b3df9e 100755 --- a/deploy +++ b/deploy @@ -8,6 +8,9 @@ rsync -avz --no-t --no-p --delete \ --exclude 'data/' \ index.html api.php containers.json og-image.png ${HOST}:${DIR} +# Deploy data directory protection +scp data/index.php ${HOST}:${DIR}/data/index.php 2>/dev/null || true + # Handle data files if [ "$1" = "--reset-data" ]; then echo "Pushing local data to server..." diff --git a/index.html b/index.html index ca3374f..dabb89c 100644 --- a/index.html +++ b/index.html @@ -790,6 +790,7 @@ let inventory = []; let isUnlocked = false; + let currentPin = null; let currentFilter = 'all'; let containerFilter = null; let hasPinSet = false; @@ -1080,6 +1081,7 @@ function toggleLock() { if (isUnlocked) { isUnlocked = false; + currentPin = null; updateLockUI(); showToast('Locked'); } else { @@ -1110,6 +1112,7 @@ if (result.success) { isUnlocked = true; + currentPin = pin; updateLockUI(); closeModal('pinModal'); document.getElementById('pinInput').value = ''; @@ -1149,6 +1152,7 @@ if (result.success) { hasPinSet = true; isUnlocked = true; + currentPin = newPin; updateLockUI(); closeModal('setPinModal'); document.getElementById('newPinInput').value = ''; @@ -1231,9 +1235,10 @@ id: parseInt(editingId), name, container, - outOfStock + outOfStock, + pin: currentPin }); - + const item = inventory.find(i => i.id === parseInt(editingId)); if (item) { item.name = name; @@ -1243,7 +1248,7 @@ showToast('Item updated'); } else { // Add new - const result = await apiCall('add', { name, container, outOfStock }); + const result = await apiCall('add', { name, container, outOfStock, pin: currentPin }); if (result.success && result.item) { inventory.push(result.item); @@ -1270,7 +1275,7 @@ showLoading(); try { - await apiCall('delete', { id: parseInt(editingId) }); + await apiCall('delete', { id: parseInt(editingId), pin: currentPin }); inventory = inventory.filter(i => i.id !== parseInt(editingId)); renderInventory();