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:
Eric Wagoner
2025-12-23 22:30:39 -05:00
parent 08d550b0bd
commit 7e523392c0
3 changed files with 63 additions and 13 deletions

60
api.php
View File

@@ -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;