Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 28 additions & 12 deletions flight/Engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,12 @@ public function init(): void
$this->set('flight.case_sensitive', false);
$this->set('flight.handle_errors', true);
$this->set('flight.log_errors', false);
$this->set('flight.debug', false);
$this->set('flight.views.path', './views');
$this->set('flight.views.extension', '.php');
$this->set('flight.content_length', true);
$this->set('flight.v2.output_buffering', false);
$this->set('flight.allow_method_override', true);

// Startup configuration
$this->before('start', function () use ($self) {
Expand All @@ -225,6 +227,8 @@ public function init(): void
// which causes a lot of problems. This will be removed
// in v4
$self->response()->v2_output_buffering = $this->get('flight.v2.output_buffering');
// Propagate method override setting to Request
$self->request()::$allowMethodOverride = (bool) $self->get('flight.allow_method_override');
});

$this->initialized = true;
Expand Down Expand Up @@ -678,16 +682,24 @@ public function _start(): void
public function _error(Throwable $e): void
{
$this->triggerEvent('flight.error', $e);
$msg = sprintf(
<<<'HTML'
<h1>500 Internal Server Error</h1>
<h3>%s (%s)</h3>
<pre>%s</pre>
HTML, // phpcs:ignore
$e->getMessage(),
$e->getCode(),
$e->getTraceAsString()
);

if ($this->get('flight.debug') === true) {
$msg = sprintf(
<<<'HTML'
<h1>500 Internal Server Error</h1>
<h3>%s (%s)</h3>
<pre>%s</pre>
HTML, // phpcs:ignore
htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'),
$e->getCode(),
htmlspecialchars($e->getTraceAsString(), ENT_QUOTES, 'UTF-8')
);
} else {
if ($this->get('flight.log_errors') === true) {
error_log($e->getMessage() . "\n" . $e->getTraceAsString());
}
$msg = '<h1>500 Internal Server Error</h1>';
}

try {
$this->response()
Expand Down Expand Up @@ -890,7 +902,7 @@ public function _redirect(string $url, int $code = 303): void
}

// Append base url to redirect url
if ($base !== '/' && strpos($url, '://') === false) {
if ($base !== '/' && strpos($url, '://') === false) {
$url = $base . preg_replace('#/+#', '/', '/' . $url);
}

Expand Down Expand Up @@ -1001,7 +1013,11 @@ public function _jsonp(
int $option = 0
): void {
$json = $encode ? Json::encode($data, $option) : $data;
$callback = $this->request()->query[$param];
$callback = (string) $this->request()->query[$param];

if ($callback !== '' && !preg_match('/^[A-Za-z_$][\w$.]{0,127}$/', $callback)) {
throw new Exception('Invalid JSONP callback name.');
}

$this->response()
->status($code)
Expand Down
7 changes: 7 additions & 0 deletions flight/commands/ControllerCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ public function execute(string $controller): void
return;
}

$controller = basename($controller);

if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', str_replace('Controller', '', $controller))) {
$io->error('Controller name must contain only letters, numbers, and underscores.', true);
return;
}

if (!preg_match('/Controller$/', $controller)) {
$controller .= 'Controller';
}
Expand Down
26 changes: 26 additions & 0 deletions flight/database/SimplePdo.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ public function __construct(
}
}

/**
* Validates that an SQL identifier (table or column name) is safe for interpolation.
* Throws PDOException on invalid identifier to prevent SQL injection.
*/
protected function requireSafeIdentifier(string $identifier): void
{
if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $identifier)) {
throw new PDOException("Unsafe SQL identifier: '$identifier'");
}
}

/**
* Pulls one row from the query
*
Expand Down Expand Up @@ -319,6 +330,8 @@ public function transaction(callable $callback)
*/
public function insert(string $table, array $data): string
{
$this->requireSafeIdentifier($table);

// Detect if this is a bulk insert (array of arrays)
$isBulk = isset($data[0]) && is_array($data[0]);

Expand All @@ -333,6 +346,10 @@ public function insert(string $table, array $data): string
$columns = array_keys($firstRow);
$columnCount = count($columns);

foreach ($columns as $col) {
$this->requireSafeIdentifier((string) $col);
}

// Validate all rows have same columns
foreach ($data as $index => $row) {
if (count($row) !== $columnCount) {
Expand Down Expand Up @@ -363,6 +380,11 @@ public function insert(string $table, array $data): string
} else {
// Single insert
$columns = array_keys($data);

foreach ($columns as $col) {
$this->requireSafeIdentifier((string) $col);
}

$placeholders = array_fill(0, count($data), '?');

$sql = sprintf(
Expand Down Expand Up @@ -396,8 +418,11 @@ public function insert(string $table, array $data): string
*/
public function update(string $table, array $data, string $where, array $whereParams = []): int
{
$this->requireSafeIdentifier($table);

$sets = [];
foreach (array_keys($data) as $column) {
$this->requireSafeIdentifier((string) $column);
$sets[] = "$column = ?";
}

Expand Down Expand Up @@ -426,6 +451,7 @@ public function update(string $table, array $data, string $where, array $wherePa
*/
public function delete(string $table, string $where, array $whereParams = []): int
{
$this->requireSafeIdentifier($table);
$sql = "DELETE FROM $table WHERE $where";
$stmt = $this->runQuery($sql, $whereParams);
return $stmt->rowCount();
Expand Down
16 changes: 12 additions & 4 deletions flight/net/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ class Request
*/
public string $servername;

/**
* Whether to allow HTTP method override via X-HTTP-Method-Override header or _method POST field.
* Controlled by the flight.allow_method_override engine setting.
*/
public static bool $allowMethodOverride = true;

/**
* Stream path for where to pull the request body from
*/
Expand Down Expand Up @@ -282,10 +288,12 @@ public static function getMethod(): string
{
$method = self::getVar('REQUEST_METHOD', 'GET');

if (self::getVar('HTTP_X_HTTP_METHOD_OVERRIDE') !== '') {
$method = self::getVar('HTTP_X_HTTP_METHOD_OVERRIDE');
} elseif (isset($_REQUEST['_method']) === true) {
$method = $_REQUEST['_method'];
if (self::$allowMethodOverride === true) {
if (self::getVar('HTTP_X_HTTP_METHOD_OVERRIDE') !== '') {
$method = self::getVar('HTTP_X_HTTP_METHOD_OVERRIDE');
} elseif (isset($_REQUEST['_method']) === true) {
$method = $_REQUEST['_method'];
}
}

return strtoupper($method);
Expand Down
8 changes: 8 additions & 0 deletions tests/EngineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ public function testHandleErrorWithException(): void
public function testHandleException(): void
{
$engine = new Engine();
$this->expectOutputRegex('~\<h1\>500 Internal Server Error\</h1\>~');
$engine->handleException(new Exception('thrown exception message', 20));
}

public function testHandleExceptionDebugMode(): void
{
$engine = new Engine();
$engine->set('flight.debug', true);
$this->expectOutputRegex('~\<h1\>500 Internal Server Error\</h1\>[\s\S]*\<h3\>thrown exception message \(20\)\</h3\>~');
$engine->handleException(new Exception('thrown exception message', 20));
}
Expand Down
Loading