From 0abf82e631599bf3477a3e4aa9a0f8aefd4880e6 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:24:05 +0200 Subject: [PATCH] fix(api): limit pull request ids to the database integer range A pull request id above 2147483647 caused a database error and a 500. - Previews store: validate max 2147483647 (422) - /deploy: return 400 for pr or pull_request_id out of range - Preview show, logs, update, and delete: return 422 for ids out of range - Document the maximum in the OpenAPI spec --- .../Api/ApplicationPreviewsController.php | 6 +- .../Api/ApplicationsController.php | 6 +- app/Http/Controllers/Api/DeployController.php | 7 +- openapi.json | 7 +- openapi.yaml | 3 + .../Api/PreviewPullRequestIdRangeApiTest.php | 92 +++++++++++++++++++ 6 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 tests/Feature/Api/PreviewPullRequestIdRangeApiTest.php diff --git a/app/Http/Controllers/Api/ApplicationPreviewsController.php b/app/Http/Controllers/Api/ApplicationPreviewsController.php index 1e82df5816..453d605695 100644 --- a/app/Http/Controllers/Api/ApplicationPreviewsController.php +++ b/app/Http/Controllers/Api/ApplicationPreviewsController.php @@ -91,7 +91,7 @@ class ApplicationPreviewsController extends Controller $this->authorize('view', $application); $pullRequestIdRaw = $request->route('pull_request_id'); - if (! ctype_digit((string) $pullRequestIdRaw) || (int) $pullRequestIdRaw <= 0) { + if (! ctype_digit((string) $pullRequestIdRaw) || (int) $pullRequestIdRaw <= 0 || (int) $pullRequestIdRaw > 2147483647) { return response()->json(['message' => 'Invalid pull_request_id.'], 422); } @@ -116,7 +116,7 @@ class ApplicationPreviewsController extends Controller requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent( required: ['pull_request_id'], properties: [ - new OA\Property(property: 'pull_request_id', type: 'integer', minimum: 1, example: 42), + new OA\Property(property: 'pull_request_id', type: 'integer', minimum: 1, maximum: 2147483647, example: 42), new OA\Property(property: 'pull_request_html_url', type: 'string', nullable: true, example: 'https://github.com/org/repo/pull/42'), new OA\Property(property: 'git_type', type: 'string', nullable: true, enum: self::GIT_TYPES, description: 'Git provider that hosts the pull request. Sets the ref Coolify fetches (pull/{id}/head or merge-requests/{id}/head).'), new OA\Property(property: 'commit', type: 'string', nullable: true, description: 'Commit SHA to deploy. Required for Bitbucket.'), @@ -186,7 +186,7 @@ class ApplicationPreviewsController extends Controller } $rules = [ - 'pull_request_id' => 'required|integer|min:1', + 'pull_request_id' => 'required|integer|min:1|max:2147483647', 'pull_request_html_url' => 'nullable|url|max:2048', 'force' => 'boolean', 'instant_deploy' => 'boolean', diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 0886ca812f..8513dc1180 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -2621,7 +2621,7 @@ class ApplicationsController extends Controller $pullRequestId = null; $pullRequestIdRaw = $request->route('pull_request_id'); if ($pullRequestIdRaw !== null) { - if (! ctype_digit((string) $pullRequestIdRaw) || (int) $pullRequestIdRaw <= 0) { + if (! ctype_digit((string) $pullRequestIdRaw) || (int) $pullRequestIdRaw <= 0 || (int) $pullRequestIdRaw > 2147483647) { return response()->json(['message' => 'Invalid pull_request_id.'], 422); } $pullRequestId = (int) $pullRequestIdRaw; @@ -2728,7 +2728,7 @@ class ApplicationsController extends Controller $this->authorize('update', $application); $pullRequestIdRaw = $request->route('pull_request_id'); - if (! ctype_digit((string) $pullRequestIdRaw) || (int) $pullRequestIdRaw <= 0) { + if (! ctype_digit((string) $pullRequestIdRaw) || (int) $pullRequestIdRaw <= 0 || (int) $pullRequestIdRaw > 2147483647) { return response()->json(['message' => 'Invalid pull_request_id.'], 422); } @@ -5655,7 +5655,7 @@ class ApplicationsController extends Controller $this->authorize('delete', $application); $pullRequestIdRaw = $request->route('pull_request_id'); - if (! ctype_digit((string) $pullRequestIdRaw) || (int) $pullRequestIdRaw <= 0) { + if (! ctype_digit((string) $pullRequestIdRaw) || (int) $pullRequestIdRaw <= 0 || (int) $pullRequestIdRaw > 2147483647) { return response()->json(['message' => 'Invalid pull_request_id.'], 422); } $pullRequestId = (int) $pullRequestIdRaw; diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index e5aac43b85..fc7a9d5460 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -315,8 +315,8 @@ class DeployController extends Controller new OA\Parameter(name: 'tag', in: 'query', description: 'Tag name(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')), new OA\Parameter(name: 'uuid', in: 'query', description: 'Resource UUID(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')), new OA\Parameter(name: 'force', in: 'query', description: 'Force rebuild (without cache)', schema: new OA\Schema(type: 'boolean')), - new OA\Parameter(name: 'pr', in: 'query', description: 'Pull Request Id for deploying specific PR builds. Cannot be used with tag parameter.', schema: new OA\Schema(type: 'integer')), - new OA\Parameter(name: 'pull_request_id', in: 'query', description: 'Preview deployment identifier. Alias of pr.', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'pr', in: 'query', description: 'Pull Request Id for deploying specific PR builds. Cannot be used with tag parameter.', schema: new OA\Schema(type: 'integer', maximum: 2147483647)), + new OA\Parameter(name: 'pull_request_id', in: 'query', description: 'Preview deployment identifier. Alias of pr.', schema: new OA\Schema(type: 'integer', maximum: 2147483647)), new OA\Parameter(name: 'docker_tag', in: 'query', description: 'Docker image tag for Docker Image preview deployments. Requires pull_request_id.', schema: new OA\Schema(type: 'string')), ], @@ -372,6 +372,9 @@ class DeployController extends Controller $pr = $pullRequestId ? max((int) $pullRequestId, 0) : 0; $dockerTag = $request->string('docker_tag')->trim()->value() ?: null; + if ($pr > 2147483647) { + return response()->json(['message' => 'pull_request_id must be between 1 and 2147483647.'], 400); + } if ($uuids && $tags) { return response()->json(['message' => 'You can only use uuid or tag, not both.'], 400); } diff --git a/openapi.json b/openapi.json index 1f0537a281..a500a6d03f 100644 --- a/openapi.json +++ b/openapi.json @@ -86,6 +86,7 @@ "properties": { "pull_request_id": { "type": "integer", + "maximum": 2147483647, "minimum": 1, "example": 42 }, @@ -10274,7 +10275,8 @@ "in": "query", "description": "Pull Request Id for deploying specific PR builds. Cannot be used with tag parameter.", "schema": { - "type": "integer" + "type": "integer", + "maximum": 2147483647 } }, { @@ -10282,7 +10284,8 @@ "in": "query", "description": "Preview deployment identifier. Alias of pr.", "schema": { - "type": "integer" + "type": "integer", + "maximum": 2147483647 } }, { diff --git a/openapi.yaml b/openapi.yaml index 69f2a33bc3..16cbdc4a30 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -62,6 +62,7 @@ paths: properties: pull_request_id: type: integer + maximum: 2147483647 minimum: 1 example: 42 pull_request_html_url: @@ -6722,12 +6723,14 @@ paths: description: 'Pull Request Id for deploying specific PR builds. Cannot be used with tag parameter.' schema: type: integer + maximum: 2147483647 - name: pull_request_id in: query description: 'Preview deployment identifier. Alias of pr.' schema: type: integer + maximum: 2147483647 - name: docker_tag in: query diff --git a/tests/Feature/Api/PreviewPullRequestIdRangeApiTest.php b/tests/Feature/Api/PreviewPullRequestIdRangeApiTest.php new file mode 100644 index 0000000000..ce83a938fc --- /dev/null +++ b/tests/Feature/Api/PreviewPullRequestIdRangeApiTest.php @@ -0,0 +1,92 @@ +set('app.maintenance.store', 'array'); + InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0])); + + $this->team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + session(['currentTeam' => $this->team]); + + $this->bearerToken = $this->user->createToken('preview-range', ['*'])->plainTextToken; + $server = Server::factory()->create(['team_id' => $this->team->id]); + $destination = StandaloneDocker::where('server_id', $server->id)->first(); + $project = Project::factory()->create(['team_id' => $this->team->id]); + $environment = Environment::factory()->create(['project_id' => $project->id]); + $this->application = Application::factory()->create([ + 'build_pack' => 'dockerimage', + 'docker_registry_image_name' => 'nginx', + 'docker_registry_image_tag' => 'alpine', + 'environment_id' => $environment->id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + ]); +}); + +test('creating a preview rejects a pull request id above the database integer range', function () { + $this->withToken($this->bearerToken) + ->postJson("/api/v1/applications/{$this->application->uuid}/previews", [ + 'pull_request_id' => 2147483648, + 'docker_tag' => 'pr-1', + 'instant_deploy' => false, + ]) + ->assertUnprocessable() + ->assertJsonValidationErrors('pull_request_id'); + + expect($this->application->previews()->count())->toBe(0); +}); + +test('creating a preview accepts the largest database integer pull request id', function () { + $this->withToken($this->bearerToken) + ->postJson("/api/v1/applications/{$this->application->uuid}/previews", [ + 'pull_request_id' => 2147483647, + 'docker_tag' => 'pr-1', + 'instant_deploy' => false, + ]) + ->assertCreated() + ->assertJsonPath('preview.pull_request_id', 2147483647); +}); + +test('deploy rejects a pull request id above the database integer range', function (string $field) { + $this->withToken($this->bearerToken) + ->postJson('/api/v1/deploy', ['uuid' => $this->application->uuid, $field => '2147483648']) + ->assertStatus(400) + ->assertJsonPath('message', 'pull_request_id must be between 1 and 2147483647.'); + + Bus::assertNothingDispatched(); +})->with(['pr', 'pull_request_id']); + +test('deploy passes the largest database integer pull request id to the preview lookup', function () { + $this->withToken($this->bearerToken) + ->postJson('/api/v1/deploy', ['uuid' => $this->application->uuid, 'pr' => 2147483647]) + ->assertOk() + ->assertJsonPath('deployments.0.message', 'Pull request 2147483647 not found for this resource.'); +}); + +test('preview routes reject a pull request id above the database integer range', function (string $method, string $suffix) { + $this->withToken($this->bearerToken) + ->json($method, "/api/v1/applications/{$this->application->uuid}/previews/2147483648{$suffix}") + ->assertUnprocessable() + ->assertJsonPath('message', 'Invalid pull_request_id.'); +})->with([ + 'show' => ['GET', ''], + 'update' => ['PATCH', ''], + 'delete' => ['DELETE', ''], + 'logs' => ['GET', '/logs'], +]);