fix: harden PHP type handling across all endpoints

This commit is contained in:
Alexander Schmidt
2026-03-26 07:57:11 +01:00
parent 5d38946c53
commit 2263fbf659
7 changed files with 97 additions and 37 deletions

View File

@@ -33,7 +33,10 @@ function verify_origin(): void {
function get_hmac_secret(): string {
$secretFile = __DIR__ . '/../data/secret.key';
if (file_exists($secretFile)) {
return trim(file_get_contents($secretFile));
$raw = file_get_contents($secretFile);
if (is_string($raw) && $raw !== '') {
return trim($raw);
}
}
$secret = bin2hex(random_bytes(32));
$dir = dirname($secretFile);
@@ -53,8 +56,10 @@ function check_rate_limit(string $action, int $limit, int $window_seconds): bool
$now = time();
$times = [];
if (file_exists($rateFile)) {
$times = json_decode(file_get_contents($rateFile), true) ?: [];
$times = array_values(array_filter($times, fn($t) => $t > $now - $window_seconds));
$raw = file_get_contents($rateFile);
$decoded = is_string($raw) ? json_decode($raw, true) : [];
$times = is_array($decoded) ? $decoded : [];
$times = array_values(array_filter($times, fn($t) => is_numeric($t) && (int)$t > $now - $window_seconds));
}
if (count($times) >= $limit) return false;
$times[] = $now;
@@ -68,9 +73,15 @@ function read_json_locked(string $file): array {
$dir = dirname($file);
if (!is_dir($dir)) mkdir($dir, 0750, true);
$fp = fopen($file, 'c+');
if ($fp === false) {
throw new RuntimeException('Unable to open file: ' . $file);
}
flock($fp, LOCK_EX);
$size = filesize($file);
$data = ($size > 0) ? (json_decode(fread($fp, $size), true) ?: []) : [];
$size = is_int($size) ? $size : 0;
$raw = $size > 0 ? fread($fp, $size) : '';
$decoded = is_string($raw) ? json_decode($raw, true) : [];
$data = is_array($decoded) ? $decoded : [];
return [$fp, $data];
}

View File

@@ -18,7 +18,7 @@ if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
exit;
}
$code = $_GET['code'] ?? '';
$code = isset($_GET['code']) && is_string($_GET['code']) ? $_GET['code'] : '';
if (empty($code) || !preg_match('/^[a-z0-9]{4,10}$/', $code)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid code']);
@@ -32,7 +32,9 @@ if (!file_exists($dbFile)) {
exit;
}
$urls = json_decode(file_get_contents($dbFile), true) ?: [];
$rawUrls = file_get_contents($dbFile);
$decodedUrls = is_string($rawUrls) ? json_decode($rawUrls, true) : [];
$urls = is_array($decodedUrls) ? $decodedUrls : [];
if (!isset($urls[$code])) {
http_response_code(404);
echo json_encode(['error' => 'Invoice not found']);
@@ -40,7 +42,8 @@ if (!isset($urls[$code])) {
}
$data = $urls[$code];
$hash = is_array($data) ? $data['h'] : $data;
$hash = is_array($data) ? ($data['h'] ?? '') : $data;
$hash = is_string($hash) ? $hash : '';
$signature = is_array($data) ? $data['s'] : null;
// Re-derive expected signature so client can verify

View File

@@ -40,9 +40,11 @@ $now = time();
$rateData = [];
if (file_exists($rateFile)) {
$rateData = json_decode(file_get_contents($rateFile), true) ?: [];
$rawRate = file_get_contents($rateFile);
$decodedRate = is_string($rawRate) ? json_decode($rawRate, true) : [];
$rateData = is_array($decodedRate) ? $decodedRate : [];
// Clean old entries
$rateData = array_filter($rateData, fn($t) => $t > $now - 60);
$rateData = array_values(array_filter($rateData, fn($t) => is_numeric($t) && (int)$t > $now - 60));
}
if (count($rateData) >= 60) {
@@ -55,15 +57,16 @@ $rateData[] = $now;
file_put_contents($rateFile, json_encode($rateData));
// Parse request
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || !isset($input['method'])) {
$rawInput = file_get_contents('php://input');
$input = is_string($rawInput) ? json_decode($rawInput, true) : null;
if (!is_array($input) || !isset($input['method']) || !is_string($input['method'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing method']);
exit;
}
$method = $input['method'];
$params = $input['params'] ?? [];
$params = isset($input['params']) && is_array($input['params']) ? $input['params'] : [];
// Determine endpoint type
$isJsonRpc = in_array($method, $ALLOWED_JSON_RPC);
@@ -79,8 +82,9 @@ if (!$isJsonRpc && !$isHttp) {
$cacheFile = __DIR__ . '/../data/node_cache.json';
$cachedNode = null;
if (file_exists($cacheFile)) {
$cache = json_decode(file_get_contents($cacheFile), true);
if ($cache && ($cache['time'] ?? 0) > $now - 300) {
$rawCache = file_get_contents($cacheFile);
$cache = is_string($rawCache) ? json_decode($rawCache, true) : null;
if (is_array($cache) && ($cache['time'] ?? 0) > $now - 300 && isset($cache['node']) && is_string($cache['node'])) {
$cachedNode = $cache['node'];
}
}
@@ -108,6 +112,10 @@ foreach ($orderedNodes as $node) {
}
$ch = curl_init($url);
if ($ch === false) {
$lastError = 'cURL init failed';
continue;
}
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,

View File

@@ -6,8 +6,10 @@ $cacheFile = __DIR__ . '/../data/rates_cache.json';
$cacheTTL = 120; // seconds
if (file_exists($cacheFile)) {
$cached = json_decode(file_get_contents($cacheFile), true);
if ($cached && (time() - ($cached['_time'] ?? 0)) < $cacheTTL) {
$rawCached = file_get_contents($cacheFile);
$cached = is_string($rawCached) ? json_decode($rawCached, true) : null;
$cachedTime = is_array($cached) && isset($cached['_time']) && is_numeric($cached['_time']) ? (int)$cached['_time'] : 0;
if (is_array($cached) && (time() - $cachedTime) < $cacheTTL) {
unset($cached['_time']);
header('Cache-Control: public, max-age=60');
echo json_encode($cached);
@@ -15,10 +17,15 @@ if (file_exists($cacheFile)) {
}
}
$currencies = $_GET['c'] ?? 'eur,usd,chf,gbp,jpy,rub,brl';
$currencies = isset($_GET['c']) && is_string($_GET['c']) ? $_GET['c'] : 'eur,usd,chf,gbp,jpy,rub,brl';
$currencies = preg_replace('/[^a-z,]/', '', strtolower($currencies));
$url = 'https://api.coingecko.com/api/v3/simple/price?ids=monero&vs_currencies=' . $currencies;
$ch = curl_init($url);
if ($ch === false) {
http_response_code(502);
echo json_encode(['error' => 'Failed to initialize request']);
exit;
}
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
@@ -30,7 +37,7 @@ curl_close($ch);
if ($response !== false && $httpCode === 200) {
$data = json_decode($response, true);
if ($data) {
if (is_array($data)) {
$data['_time'] = time();
file_put_contents($cacheFile, json_encode($data));
unset($data['_time']);
@@ -42,8 +49,9 @@ if ($response !== false && $httpCode === 200) {
// On error, serve stale cache if available
if (file_exists($cacheFile)) {
$cached = json_decode(file_get_contents($cacheFile), true);
if ($cached) {
$rawCached = file_get_contents($cacheFile);
$cached = is_string($rawCached) ? json_decode($rawCached, true) : null;
if (is_array($cached)) {
unset($cached['_time']);
header('Cache-Control: public, max-age=30');
echo json_encode($cached);

View File

@@ -21,8 +21,9 @@ if (!check_rate_limit('shorten', 20, 3600)) {
$dbFile = __DIR__ . '/../data/urls.json';
$input = json_decode(file_get_contents('php://input'), true);
$hash = $input['hash'] ?? '';
$rawInput = file_get_contents('php://input');
$input = is_string($rawInput) ? json_decode($rawInput, true) : null;
$hash = is_array($input) && isset($input['hash']) && is_string($input['hash']) ? $input['hash'] : '';
if (empty($hash) || strlen($hash) > 500 || !preg_match('/^[a-zA-Z0-9%+_=&.-]{1,500}$/', $hash)) {
http_response_code(400);
@@ -33,10 +34,16 @@ if (empty($hash) || strlen($hash) > 500 || !preg_match('/^[a-zA-Z0-9%+_=&.-]{1,5
$secret = get_hmac_secret();
[$fp, $urls] = read_json_locked($dbFile);
if (!is_array($urls)) {
$urls = [];
}
// Check if this hash already exists
foreach ($urls as $code => $data) {
$stored_hash = is_array($data) ? $data['h'] : $data;
$stored_hash = is_array($data) ? ($data['h'] ?? null) : $data;
if (!is_string($stored_hash)) {
continue;
}
if ($stored_hash === $hash) {
flock($fp, LOCK_UN);
fclose($fp);

View File

@@ -22,14 +22,25 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
echo json_encode(['error' => 'Rate limit exceeded']);
exit;
}
$code = $_GET['code'] ?? '';
$code = isset($_GET['code']) && is_string($_GET['code']) ? $_GET['code'] : '';
if (empty($code) || !preg_match('/^[a-z0-9]{4,10}$/', $code)) {
echo json_encode(['verified' => false]);
exit;
}
$proofs = file_exists($dbFile) ? (json_decode(file_get_contents($dbFile), true) ?: []) : [];
$rawProofs = file_exists($dbFile) ? file_get_contents($dbFile) : null;
$decodedProofs = is_string($rawProofs) ? json_decode($rawProofs, true) : [];
$proofs = is_array($decodedProofs) ? $decodedProofs : [];
if (isset($proofs[$code])) {
echo json_encode(array_merge(['verified' => true], $proofs[$code]));
$response = ['verified' => true];
$proofEntry = $proofs[$code];
if (is_array($proofEntry)) {
foreach ($proofEntry as $k => $v) {
if (is_string($k)) {
$response[$k] = $v;
}
}
}
echo json_encode($response);
} else {
echo json_encode(['verified' => false]);
}
@@ -51,15 +62,16 @@ if (!check_rate_limit('verify_post', 10, 3600)) {
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
$rawInput = file_get_contents('php://input');
$input = is_string($rawInput) ? json_decode($rawInput, true) : null;
if (!is_array($input)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON']);
exit;
}
$code = $input['code'] ?? '';
$txHash = $input['tx_hash'] ?? '';
$code = isset($input['code']) && is_string($input['code']) ? $input['code'] : '';
$txHash = isset($input['tx_hash']) && is_string($input['tx_hash']) ? $input['tx_hash'] : '';
$amount = floatval($input['amount'] ?? 0);
$confirmations = intval($input['confirmations'] ?? 0);
@@ -82,7 +94,9 @@ if (!file_exists($urlsFile)) {
echo json_encode(['error' => 'Invoice not found']);
exit;
}
$urls = json_decode(file_get_contents($urlsFile), true) ?: [];
$rawUrls = file_get_contents($urlsFile);
$decodedUrls = is_string($rawUrls) ? json_decode($rawUrls, true) : [];
$urls = is_array($decodedUrls) ? $decodedUrls : [];
if (!isset($urls[$code])) {
http_response_code(404);
echo json_encode(['error' => 'Invoice not found']);
@@ -91,6 +105,9 @@ if (!isset($urls[$code])) {
// Store proof with atomic lock
[$fp, $proofs] = read_json_locked($dbFile);
if (!is_array($proofs)) {
$proofs = [];
}
$status = ($input['status'] ?? 'paid') === 'pending' ? 'pending' : 'paid';