Add backend PIN verification and security hardening
- 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 <noreply@anthropic.com>
This commit is contained in:
52
api.php
52
api.php
@@ -7,7 +7,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
header('Access-Control-Allow-Origin: *');
|
|
||||||
|
// CORS - restrict to same origin (remove for local development if needed)
|
||||||
|
$allowedOrigins = [
|
||||||
|
'https://pantry.kestrelsnest.social',
|
||||||
|
'http://localhost:8000'
|
||||||
|
];
|
||||||
|
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||||
|
if (in_array($origin, $allowedOrigins)) {
|
||||||
|
header("Access-Control-Allow-Origin: $origin");
|
||||||
|
}
|
||||||
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
|
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
|
||||||
header('Access-Control-Allow-Headers: Content-Type');
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
|
||||||
@@ -135,8 +144,26 @@ function getNextId($inventory) {
|
|||||||
/**
|
/**
|
||||||
* Sanitize string input
|
* Sanitize string input
|
||||||
*/
|
*/
|
||||||
function sanitize($str) {
|
function sanitize($str, $maxLength = 200) {
|
||||||
return trim($str);
|
$str = trim($str);
|
||||||
|
if (strlen($str) > $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
|
// Main request handling
|
||||||
@@ -195,8 +222,13 @@ switch ($action) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'add':
|
case 'add':
|
||||||
|
if (!requirePin($input)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid PIN']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
$name = sanitize($input['name'] ?? '');
|
$name = sanitize($input['name'] ?? '');
|
||||||
$container = sanitize($input['container'] ?? 'glass-bail-medium');
|
$container = sanitize($input['container'] ?? 'glass-bail-medium', 50);
|
||||||
$outOfStock = (bool)($input['outOfStock'] ?? false);
|
$outOfStock = (bool)($input['outOfStock'] ?? false);
|
||||||
|
|
||||||
if (empty($name)) {
|
if (empty($name)) {
|
||||||
@@ -219,9 +251,14 @@ switch ($action) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'update':
|
case 'update':
|
||||||
|
if (!requirePin($input)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid PIN']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
$id = (int)($input['id'] ?? 0);
|
$id = (int)($input['id'] ?? 0);
|
||||||
$name = sanitize($input['name'] ?? '');
|
$name = sanitize($input['name'] ?? '');
|
||||||
$container = sanitize($input['container'] ?? '');
|
$container = sanitize($input['container'] ?? '', 50);
|
||||||
$outOfStock = (bool)($input['outOfStock'] ?? false);
|
$outOfStock = (bool)($input['outOfStock'] ?? false);
|
||||||
|
|
||||||
if ($id <= 0) {
|
if ($id <= 0) {
|
||||||
@@ -252,6 +289,11 @@ switch ($action) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'delete':
|
case 'delete':
|
||||||
|
if (!requirePin($input)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid PIN']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
$id = (int)($input['id'] ?? 0);
|
$id = (int)($input['id'] ?? 0);
|
||||||
|
|
||||||
if ($id <= 0) {
|
if ($id <= 0) {
|
||||||
|
|||||||
3
deploy
3
deploy
@@ -8,6 +8,9 @@ rsync -avz --no-t --no-p --delete \
|
|||||||
--exclude 'data/' \
|
--exclude 'data/' \
|
||||||
index.html api.php containers.json og-image.png ${HOST}:${DIR}
|
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
|
# Handle data files
|
||||||
if [ "$1" = "--reset-data" ]; then
|
if [ "$1" = "--reset-data" ]; then
|
||||||
echo "Pushing local data to server..."
|
echo "Pushing local data to server..."
|
||||||
|
|||||||
11
index.html
11
index.html
@@ -790,6 +790,7 @@
|
|||||||
|
|
||||||
let inventory = [];
|
let inventory = [];
|
||||||
let isUnlocked = false;
|
let isUnlocked = false;
|
||||||
|
let currentPin = null;
|
||||||
let currentFilter = 'all';
|
let currentFilter = 'all';
|
||||||
let containerFilter = null;
|
let containerFilter = null;
|
||||||
let hasPinSet = false;
|
let hasPinSet = false;
|
||||||
@@ -1080,6 +1081,7 @@
|
|||||||
function toggleLock() {
|
function toggleLock() {
|
||||||
if (isUnlocked) {
|
if (isUnlocked) {
|
||||||
isUnlocked = false;
|
isUnlocked = false;
|
||||||
|
currentPin = null;
|
||||||
updateLockUI();
|
updateLockUI();
|
||||||
showToast('Locked');
|
showToast('Locked');
|
||||||
} else {
|
} else {
|
||||||
@@ -1110,6 +1112,7 @@
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
isUnlocked = true;
|
isUnlocked = true;
|
||||||
|
currentPin = pin;
|
||||||
updateLockUI();
|
updateLockUI();
|
||||||
closeModal('pinModal');
|
closeModal('pinModal');
|
||||||
document.getElementById('pinInput').value = '';
|
document.getElementById('pinInput').value = '';
|
||||||
@@ -1149,6 +1152,7 @@
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
hasPinSet = true;
|
hasPinSet = true;
|
||||||
isUnlocked = true;
|
isUnlocked = true;
|
||||||
|
currentPin = newPin;
|
||||||
updateLockUI();
|
updateLockUI();
|
||||||
closeModal('setPinModal');
|
closeModal('setPinModal');
|
||||||
document.getElementById('newPinInput').value = '';
|
document.getElementById('newPinInput').value = '';
|
||||||
@@ -1231,7 +1235,8 @@
|
|||||||
id: parseInt(editingId),
|
id: parseInt(editingId),
|
||||||
name,
|
name,
|
||||||
container,
|
container,
|
||||||
outOfStock
|
outOfStock,
|
||||||
|
pin: currentPin
|
||||||
});
|
});
|
||||||
|
|
||||||
const item = inventory.find(i => i.id === parseInt(editingId));
|
const item = inventory.find(i => i.id === parseInt(editingId));
|
||||||
@@ -1243,7 +1248,7 @@
|
|||||||
showToast('Item updated');
|
showToast('Item updated');
|
||||||
} else {
|
} else {
|
||||||
// Add new
|
// 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) {
|
if (result.success && result.item) {
|
||||||
inventory.push(result.item);
|
inventory.push(result.item);
|
||||||
@@ -1270,7 +1275,7 @@
|
|||||||
showLoading();
|
showLoading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiCall('delete', { id: parseInt(editingId) });
|
await apiCall('delete', { id: parseInt(editingId), pin: currentPin });
|
||||||
|
|
||||||
inventory = inventory.filter(i => i.id !== parseInt(editingId));
|
inventory = inventory.filter(i => i.id !== parseInt(editingId));
|
||||||
renderInventory();
|
renderInventory();
|
||||||
|
|||||||
Reference in New Issue
Block a user