Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcc732e605 | ||
|
|
9d8a73c355 | ||
|
|
c78d24154a | ||
|
|
98d30cbc58 |
+11
-4
@@ -49,19 +49,26 @@ APP_DOMAIN=
|
||||
; Include all domains that can access this application
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1
|
||||
|
||||
; ============================================================================
|
||||
; REQUIRED FOR PRODUCTION -- READ BEFORE DEPLOYING -- TRUSTED_PROXIES
|
||||
; ============================================================================
|
||||
; Trusted reverse proxy IPs, comma-separated -- e.g. the Authelia/nginx proxy.
|
||||
; Set this to the IP address(es) of your reverse proxy. Authelia forward-auth
|
||||
; headers (Remote-User / Remote-Groups) and forwarded client IPs are only
|
||||
; trusted when REMOTE_ADDR is in this list.
|
||||
;
|
||||
; Leaving this EMPTY disables reverse-proxy verification entirely: the app then
|
||||
; trusts Remote-User / Remote-Groups headers from ANY source. That is unsafe if
|
||||
; the PHP backend is reachable directly (bypassing the proxy), because a client
|
||||
; can then spoof those headers and log in as an admin. Only leave it empty when
|
||||
; network topology guarantees PHP is reachable solely via the trusted proxy.
|
||||
; trusts Remote-User / Remote-Groups headers from ANY source. If the PHP
|
||||
; backend is reachable directly -- a misconfigured firewall rule, a container
|
||||
; network accidentally exposing the port, SSRF from another internal service
|
||||
; -- ANYONE can set Remote-User: admin themselves and fully impersonate any
|
||||
; user, including an admin, with ZERO authentication. Only leave it empty when
|
||||
; network topology guarantees PHP is reachable solely via the trusted proxy
|
||||
; (e.g. local development), never in a real deployment.
|
||||
;
|
||||
; Exact IP match only (no CIDR). Example (single proxy): TRUSTED_PROXIES=10.10.10.27
|
||||
; Example (multiple): TRUSTED_PROXIES=10.10.10.27,10.10.10.28
|
||||
; ============================================================================
|
||||
TRUSTED_PROXIES=
|
||||
|
||||
; Timezone (default: America/New_York)
|
||||
|
||||
@@ -447,6 +447,21 @@ APP_DOMAIN=your.domain.example
|
||||
TIMEZONE=America/New_York
|
||||
```
|
||||
|
||||
**⚠️ REQUIRED FOR PRODUCTION — `TRUSTED_PROXIES`:** This app trusts Authelia
|
||||
forward-auth headers (`Remote-User`, `Remote-Groups`, etc.) to identify who's
|
||||
logged in. `TRUSTED_PROXIES` restricts that trust to requests that actually
|
||||
came through your reverse proxy — **leaving it empty disables that check
|
||||
entirely**, and anyone who can reach the PHP backend directly (a
|
||||
misconfigured firewall rule, an exposed container port, SSRF from another
|
||||
internal service) can set `Remote-User: admin` themselves and fully
|
||||
impersonate any user with zero authentication. Set it to your reverse proxy's
|
||||
IP address(es) before deploying anywhere reachable beyond your own machine:
|
||||
```env
|
||||
TRUSTED_PROXIES=10.10.10.27
|
||||
```
|
||||
`GET /api/health.php` reports a `warning` on the `trusted_proxies` check if
|
||||
this is left empty, so it doesn't go unnoticed after deployment.
|
||||
|
||||
Matrix notification variables (all optional):
|
||||
```env
|
||||
# hookshot generic webhook URL — send events to Matrix room
|
||||
|
||||
@@ -25,7 +25,7 @@ if (!in_array($_SERVER['REQUEST_METHOD'], ['GET', 'HEAD'], true)) {
|
||||
$csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token', 'csrf_token' => CsrfMiddleware::getToken()]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ try {
|
||||
$csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token', 'csrf_token' => CsrfMiddleware::getToken()]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ try {
|
||||
$csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token', 'csrf_token' => CsrfMiddleware::getToken()]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Verify CSRF token
|
||||
$csrfToken = $input['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||
ResponseHelper::forbidden('Invalid CSRF token');
|
||||
ResponseHelper::error('Invalid CSRF token', 403, ['csrf_token' => CsrfMiddleware::getToken()]);
|
||||
}
|
||||
|
||||
// Get attachment ID
|
||||
|
||||
@@ -49,7 +49,7 @@ try {
|
||||
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token', 'csrf_token' => CsrfMiddleware::getToken()]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,8 +39,15 @@ try {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||
ob_end_clean();
|
||||
http_response_code(403);
|
||||
throw new Exception("Invalid CSRF token");
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Invalid CSRF token',
|
||||
'csrf_token' => CsrfMiddleware::getToken()
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -162,6 +162,21 @@ if ($maxExecTime === 0 || $maxExecTime >= $requirements['min_max_execution_time'
|
||||
];
|
||||
}
|
||||
|
||||
// Check 8: TRUSTED_PROXIES configured. Empty disables enforceTrustedProxy()'s
|
||||
// allowlist entirely, meaning anything that can reach this app directly can
|
||||
// spoof the Authelia forward-auth Remote-* headers and impersonate any user,
|
||||
// including an admin. Not fatal (a fresh/dev install may not sit behind a
|
||||
// proxy yet), but should never go unnoticed on a real deployment.
|
||||
if (!empty($GLOBALS['config']['TRUSTED_PROXIES'] ?? [])) {
|
||||
$checks['trusted_proxies'] = ['status' => 'ok', 'message' => 'configured'];
|
||||
} else {
|
||||
$checks['trusted_proxies'] = [
|
||||
'status' => 'warning',
|
||||
'message' => 'TRUSTED_PROXIES is empty — forward-auth headers are NOT verified; '
|
||||
. 'anything that can reach this app directly can impersonate any user'
|
||||
];
|
||||
}
|
||||
|
||||
// Calculate response time
|
||||
$responseTime = round((microtime(true) - $startTime) * 1000, 2);
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ try {
|
||||
$csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token', 'csrf_token' => CsrfMiddleware::getToken()]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ try {
|
||||
$csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token', 'csrf_token' => CsrfMiddleware::getToken()]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ try {
|
||||
$csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token', 'csrf_token' => CsrfMiddleware::getToken()]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,15 @@ try {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||
ob_end_clean();
|
||||
http_response_code(403);
|
||||
throw new Exception("Invalid CSRF token");
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Invalid CSRF token',
|
||||
'csrf_token' => CsrfMiddleware::getToken()
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' || $_SERVER['REQUEST_METHOD'] === 'DEL
|
||||
require_once dirname(__DIR__) . '/middleware/CsrfMiddleware.php';
|
||||
$csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||
ResponseHelper::forbidden('Invalid CSRF token');
|
||||
ResponseHelper::error('Invalid CSRF token', 403, ['csrf_token' => CsrfMiddleware::getToken()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
// Verify CSRF token
|
||||
$csrfToken = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||
ResponseHelper::forbidden('Invalid CSRF token');
|
||||
ResponseHelper::error('Invalid CSRF token', 403, ['csrf_token' => CsrfMiddleware::getToken()]);
|
||||
}
|
||||
|
||||
// Get ticket ID
|
||||
|
||||
+17
-39
@@ -8,9 +8,10 @@ ini_set('display_errors', 0);
|
||||
require_once __DIR__ . '/middleware/RateLimitMiddleware.php';
|
||||
RateLimitMiddleware::apply('api');
|
||||
|
||||
// Load environment variables with error check
|
||||
$envFile = __DIR__ . '/.env';
|
||||
if (!file_exists($envFile)) {
|
||||
// Early friendly JSON error if .env is missing, before config.php's own
|
||||
// (plain-text die()) handling would otherwise run — this is a JSON API
|
||||
// endpoint and must always respond with a JSON body.
|
||||
if (!file_exists(__DIR__ . '/.env')) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Configuration file not found'
|
||||
@@ -18,37 +19,17 @@ if (!file_exists($envFile)) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$envVars = parse_ini_file($envFile, false, INI_SCANNER_TYPED);
|
||||
if (!$envVars) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Invalid configuration file'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
// Load application config so UrlHelper can resolve APP_DOMAIN, and so the
|
||||
// DB connection below (via Database::getConnection()) gets the same
|
||||
// charset/timezone sync as every other endpoint instead of a hand-rolled
|
||||
// second connection.
|
||||
require_once __DIR__ . '/config/config.php';
|
||||
require_once __DIR__ . '/helpers/Database.php';
|
||||
|
||||
// Strip quotes from values if present (parse_ini_file may include them)
|
||||
foreach ($envVars as $key => $value) {
|
||||
if (is_string($value)) {
|
||||
if (
|
||||
(substr($value, 0, 1) === '"' && substr($value, -1) === '"') ||
|
||||
(substr($value, 0, 1) === "'" && substr($value, -1) === "'")
|
||||
) {
|
||||
$envVars[$key] = substr($value, 1, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Database connection with detailed error handling
|
||||
$conn = new mysqli(
|
||||
$envVars['DB_HOST'],
|
||||
$envVars['DB_USER'],
|
||||
$envVars['DB_PASS'],
|
||||
$envVars['DB_NAME']
|
||||
);
|
||||
|
||||
if ($conn->connect_error) {
|
||||
error_log('create_ticket_api: DB connection failed: ' . $conn->connect_error);
|
||||
try {
|
||||
$conn = Database::getConnection();
|
||||
} catch (\Throwable $e) {
|
||||
error_log('create_ticket_api: DB connection failed: ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
@@ -57,9 +38,6 @@ if ($conn->connect_error) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Load application config so UrlHelper can resolve APP_DOMAIN
|
||||
require_once __DIR__ . '/config/config.php';
|
||||
|
||||
// Authenticate via API key
|
||||
require_once __DIR__ . '/middleware/ApiKeyAuth.php';
|
||||
require_once __DIR__ . '/models/AuditLogModel.php';
|
||||
@@ -349,7 +327,7 @@ if ($existing) {
|
||||
(new StatsModel($conn))->invalidateCache();
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
Database::close();
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'ticket_id' => $existingId,
|
||||
@@ -386,7 +364,7 @@ if ($existing) {
|
||||
// Ticket reopened (Closed → Open) — refresh dashboard stats.
|
||||
(new StatsModel($conn))->invalidateCache();
|
||||
|
||||
$conn->close();
|
||||
Database::close();
|
||||
|
||||
require_once __DIR__ . '/helpers/NotificationHelper.php';
|
||||
NotificationHelper::sendTicketNotification($existingId, [
|
||||
@@ -484,7 +462,7 @@ if ($inserted) {
|
||||
// New ticket created — refresh dashboard stats.
|
||||
(new StatsModel($conn))->invalidateCache();
|
||||
|
||||
$conn->close();
|
||||
Database::close();
|
||||
|
||||
require_once __DIR__ . '/helpers/NotificationHelper.php';
|
||||
NotificationHelper::sendTicketNotification($ticket_id, [
|
||||
|
||||
@@ -5,6 +5,7 @@ require_once 'config/config.php';
|
||||
require_once 'middleware/SecurityHeadersMiddleware.php';
|
||||
require_once 'middleware/AuthMiddleware.php';
|
||||
require_once 'models/AuditLogModel.php';
|
||||
require_once 'helpers/Database.php';
|
||||
|
||||
// Apply security headers early
|
||||
SecurityHeadersMiddleware::apply();
|
||||
@@ -17,15 +18,12 @@ $requestPath = strtok($request, '?');
|
||||
|
||||
// Create database connection for non-API routes
|
||||
if (!str_starts_with($requestPath, '/api/')) {
|
||||
$conn = new mysqli(
|
||||
$GLOBALS['config']['DB_HOST'],
|
||||
$GLOBALS['config']['DB_USER'],
|
||||
$GLOBALS['config']['DB_PASS'],
|
||||
$GLOBALS['config']['DB_NAME']
|
||||
);
|
||||
|
||||
if ($conn->connect_error) {
|
||||
die("Connection failed: " . $conn->connect_error);
|
||||
try {
|
||||
$conn = Database::getConnection();
|
||||
} catch (\Throwable $e) {
|
||||
error_log('index.php: database connection failed: ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
die('Sorry, something went wrong. Please try again shortly.');
|
||||
}
|
||||
|
||||
// Authenticate user via Authelia forward auth
|
||||
@@ -444,5 +442,5 @@ switch (true) {
|
||||
|
||||
// Close database connection if it was opened
|
||||
if (isset($conn)) {
|
||||
$conn->close();
|
||||
Database::close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user