fix: harden PHP type handling across all endpoints
This commit is contained in:
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
22
api/node.php
22
api/node.php
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user