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:
60
api.php
60
api.php
@@ -1,13 +1,22 @@
|
||||
<?php
|
||||
/**
|
||||
* Pantry Inventory API
|
||||
*
|
||||
*
|
||||
* Simple JSON file-based backend for the pantry inventory app.
|
||||
* No database required - stores everything in inventory.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-Headers: Content-Type');
|
||||
|
||||
@@ -135,8 +144,26 @@ function getNextId($inventory) {
|
||||
/**
|
||||
* Sanitize string input
|
||||
*/
|
||||
function sanitize($str) {
|
||||
return trim($str);
|
||||
function sanitize($str, $maxLength = 200) {
|
||||
$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
|
||||
@@ -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;
|
||||
|
||||
3
deploy
3
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..."
|
||||
|
||||
13
index.html
13
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();
|
||||
|
||||
Reference in New Issue
Block a user