VYPR
High severity7.5GHSA Advisory· Published May 13, 2026· Updated May 14, 2026

CVE-2026-42551

CVE-2026-42551

Description

Flight is an extensible micro-framework for PHP. Prior to 3.18.1, Request::getMethod() unconditionally honors the X-HTTP-Method-Override header and the $_REQUEST['_method'] parameter on any HTTP verb (including safe verbs such as GET), with no opt-in and no whitelist of permitted target methods. A GET request can silently become a DELETE or PUT, enabling CSRF escalation against destructive endpoints, bypass of middleware gated on unsafe verbs, and cache poisoning between CDN and origin. This vulnerability is fixed in 3.18.1.

Affected products

1

Patches

1
b8dd23aaa828

Merge pull request #692 from flightphp/security-fixes

https://github.com/flightphp/coren0nag0nApr 22, 2026via ghsa
5 files changed · +81 16
  • flight/commands/ControllerCommand.php+7 0 modified
    @@ -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';
             }
    
  • flight/database/SimplePdo.php+26 0 modified
    @@ -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
          *
    @@ -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]);
     
    @@ -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) {
    @@ -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(
    @@ -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 = ?";
             }
     
    @@ -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();
    
  • flight/Engine.php+28 12 modified
    @@ -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) {
    @@ -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;
    @@ -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()
    @@ -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);
             }
     
    @@ -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)
    
  • flight/net/Request.php+12 4 modified
    @@ -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
          */
    @@ -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);
    
  • tests/EngineTest.php+8 0 modified
    @@ -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));
         }
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.