Harden auth session handling and SQL identifier validation

This commit is contained in:
Mikhail Chusavitin
2026-02-04 16:56:53 +03:00
parent 155b1ba9d0
commit b67bac89ee
3 changed files with 108 additions and 43 deletions

View File

@@ -13,8 +13,9 @@ $container = new Container();
AppFactory::setContainer($container);
$app = AppFactory::create();
// В dev включаем подробные ошибки (можно выключить в проде)
$app->addErrorMiddleware(true, true, true);
// Подробные ошибки только если явно включены
$displayErrors = filter_var(getenv('PHP_ERROR_DISPLAY') ?: 'false', FILTER_VALIDATE_BOOLEAN);
$app->addErrorMiddleware($displayErrors, true, true);
$container->set('db', function () {
return \App\Db::connectFromSession();
@@ -38,6 +39,7 @@ $app->post('/api/login', function (Request $request, Response $response) {
try {
\App\Db::testConnection($user, $pass);
session_regenerate_id(true);
$_SESSION['db_user'] = $user;
$_SESSION['db_pass'] = $pass;
@@ -53,10 +55,36 @@ $app->post('/api/login', function (Request $request, Response $response) {
->withStatus($status);
});
// Текущий статус сессии
$app->get('/api/session', function (Request $request, Response $response) {
$authenticated = !empty($_SESSION['db_user']) && !empty($_SESSION['db_pass']);
$payload = [
'authenticated' => $authenticated,
'user' => $authenticated ? $_SESSION['db_user'] : null,
];
$response->getBody()->write(json_encode($payload));
return $response->withHeader('Content-Type', 'application/json');
});
// Выход из системы: гарантированно очищаем серверную сессию
$app->post('/api/logout', function (Request $request, Response $response) {
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
}
session_destroy();
$response->getBody()->write(json_encode(['ok' => true]));
return $response->withHeader('Content-Type', 'application/json');
});
// Middleware на /api/*: проверяем, что аутентифицированы
$app->add(function (Request $request, $handler) {
$path = $request->getUri()->getPath();
if (str_starts_with($path, '/api/') && $path !== '/api/login') {
$publicApi = ['/api/login', '/api/session', '/api/logout'];
if (str_starts_with($path, '/api/') && !in_array($path, $publicApi, true)) {
if (empty($_SESSION['db_user']) || empty($_SESSION['db_pass'])) {
$res = new \Slim\Psr7\Response();
$res->getBody()->write(json_encode(['error' => 'Not authenticated']));
@@ -101,23 +129,13 @@ $app->post('/api/table/data', function (Request $request, Response $response) us
$pageSize = (int)($payload['size'] ?? $payload['pageSize'] ?? 50);
$filters = $payload['filters'] ?? [];
$sort = $payload['sort'] ?? null;
$columns = $payload['columns'] ?? [];
// ✅ Логирование для отладки
error_log("Data request: page=$page, pageSize=$pageSize, filters=" . json_encode($filters) . ", sort=" . json_encode($sort));
$pdo = $container->get('db');
$meta = new \App\MetaService($pdo);
$ds = new \App\DataService($pdo);
$metaArr = $meta->getTableMeta($schema, $table);
$columns = $metaArr['columns'];
$resData = $ds->fetchData($schema, $table, $columns, $page, $pageSize, $filters, $sort);
// ✅ Логирование ответа
error_log("Data response: " . json_encode([
'data_count' => count($resData['data']),
'last_page' => $resData['last_page'],
'current_page' => $resData['current_page'],
'total' => $resData['total']
]));
$response->getBody()->write(json_encode($resData));
return $response->withHeader('Content-Type', 'application/json');
});
@@ -267,7 +285,14 @@ $app->post('/api/table/export-csv', function (Request $request, Response $respon
$table = $payload['table'] ?? '';
$filters = $payload['filters'] ?? [];
$sort = $payload['sort'] ?? null;
$columns = $payload['columns'] ?? [];
$meta = new \App\MetaService($pdo);
$metaArr = $meta->getTableMeta($schema, $table);
$requestedColumns = $payload['columns'] ?? [];
$requestedNames = array_values(array_filter(array_map(fn($c) => $c['COLUMN_NAME'] ?? '', $requestedColumns)));
$columns = array_values(array_filter(
$metaArr['columns'],
fn($c) => in_array($c['COLUMN_NAME'], $requestedNames, true)
));
$result = $ds->exportCSV($schema, $table, $columns, $filters, $sort);
@@ -284,6 +309,11 @@ $app->get('/api/fk-values', function (Request $request, Response $response) use
$search = $params['search'] ?? ''; // ✅ Добавлен параметр поиска
$pdo = $container->get('db');
$isSafeIdentifier = fn(string $v): bool => preg_match('/^[A-Za-z0-9_]+$/', $v) === 1;
if (!$isSafeIdentifier($schema) || !$isSafeIdentifier($table) || !$isSafeIdentifier($column)) {
$response->getBody()->write(json_encode(['error' => 'Invalid schema/table/column name']));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
}
// ✅ Сначала проверяем количество записей
$countSql = "SELECT COUNT(DISTINCT `{$column}`) as cnt