coolify/bootstrap/helpers/api.php
Andras Bacsai a1c30cb0e7 fix(git-ref-validation): prevent command injection via git references
Add validateGitRef() helper function that uses an allowlist approach to prevent
OS command injection through git commit SHAs, branch names, and tags. Only allows
alphanumeric characters, dots, hyphens, underscores, and slashes.

Changes include:
- Add validateGitRef() helper in bootstrap/helpers/shared.php
- Apply validation in Rollback component when accepting rollback commit
- Add regex validation to git commit SHA fields in Livewire components
- Apply regex validation to API rules for git_commit_sha
- Use escapeshellarg() in git log and git checkout commands
- Add comprehensive unit tests covering injection payloads

Addresses GHSA-mw5w-2vvh-mgf4
2026-03-10 22:22:48 +01:00

194 lines
7.3 KiB
PHP

<?php
use App\Enums\BuildPackTypes;
use App\Enums\RedirectTypes;
use App\Enums\StaticImageTypes;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
function getTeamIdFromToken()
{
$token = auth()->user()->currentAccessToken();
return data_get($token, 'team_id');
}
function invalidTokenResponse()
{
return response()->json(['message' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api-reference/authorization'], 400);
}
function serializeApiResponse($data)
{
if ($data instanceof Collection) {
return $data->map(function ($d) {
$d = collect($d)->sortKeys();
$created_at = data_get($d, 'created_at');
$updated_at = data_get($d, 'updated_at');
if ($created_at) {
unset($d['created_at']);
$d['created_at'] = $created_at;
}
if ($updated_at) {
unset($d['updated_at']);
$d['updated_at'] = $updated_at;
}
if (data_get($d, 'name')) {
$d = $d->prepend($d['name'], 'name');
}
if (data_get($d, 'description')) {
$d = $d->prepend($d['description'], 'description');
}
if (data_get($d, 'uuid')) {
$d = $d->prepend($d['uuid'], 'uuid');
}
if (! is_null(data_get($d, 'id'))) {
$d = $d->prepend($d['id'], 'id');
}
return $d;
});
} else {
$d = collect($data)->sortKeys();
$created_at = data_get($d, 'created_at');
$updated_at = data_get($d, 'updated_at');
if ($created_at) {
unset($d['created_at']);
$d['created_at'] = $created_at;
}
if ($updated_at) {
unset($d['updated_at']);
$d['updated_at'] = $updated_at;
}
if (data_get($d, 'name')) {
$d = $d->prepend($d['name'], 'name');
}
if (data_get($d, 'description')) {
$d = $d->prepend($d['description'], 'description');
}
if (data_get($d, 'uuid')) {
$d = $d->prepend($d['uuid'], 'uuid');
}
if (! is_null(data_get($d, 'id'))) {
$d = $d->prepend($d['id'], 'id');
}
return $d;
}
}
function sharedDataApplications()
{
return [
'git_repository' => 'string',
'git_branch' => 'string',
'build_pack' => Rule::enum(BuildPackTypes::class),
'is_static' => 'boolean',
'is_spa' => 'boolean',
'is_auto_deploy_enabled' => 'boolean',
'is_force_https_enabled' => 'boolean',
'static_image' => Rule::enum(StaticImageTypes::class),
'domains' => 'string|nullable',
'redirect' => Rule::enum(RedirectTypes::class),
'git_commit_sha' => ['string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'],
'docker_registry_image_name' => 'string|nullable',
'docker_registry_image_tag' => 'string|nullable',
'install_command' => 'string|nullable',
'build_command' => 'string|nullable',
'start_command' => 'string|nullable',
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/',
'ports_mappings' => 'string|regex:/^(\d+:\d+)(,\d+:\d+)*$/|nullable',
'custom_network_aliases' => 'string|nullable',
'base_directory' => 'string|nullable',
'publish_directory' => 'string|nullable',
'health_check_enabled' => 'boolean',
'health_check_type' => 'string|in:http,cmd',
'health_check_command' => ['nullable', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'],
'health_check_path' => ['string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'],
'health_check_port' => 'integer|nullable|min:1|max:65535',
'health_check_host' => ['string', 'regex:/^[a-zA-Z0-9.\-_]+$/'],
'health_check_method' => 'string|in:GET,HEAD,POST,OPTIONS',
'health_check_return_code' => 'numeric',
'health_check_scheme' => 'string|in:http,https',
'health_check_response_text' => 'string|nullable',
'health_check_interval' => 'numeric',
'health_check_timeout' => 'numeric',
'health_check_retries' => 'numeric',
'health_check_start_period' => 'numeric',
'limits_memory' => 'string',
'limits_memory_swap' => 'string',
'limits_memory_swappiness' => 'numeric',
'limits_memory_reservation' => 'string',
'limits_cpus' => 'string',
'limits_cpuset' => 'string|nullable',
'limits_cpu_shares' => 'numeric',
'custom_labels' => 'string|nullable',
'custom_docker_run_options' => 'string|nullable',
'post_deployment_command' => 'string|nullable',
'post_deployment_command_container' => 'string',
'pre_deployment_command' => 'string|nullable',
'pre_deployment_command_container' => 'string',
'manual_webhook_secret_github' => 'string|nullable',
'manual_webhook_secret_gitlab' => 'string|nullable',
'manual_webhook_secret_bitbucket' => 'string|nullable',
'manual_webhook_secret_gitea' => 'string|nullable',
'dockerfile_location' => ['string', 'nullable', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'],
'docker_compose_location' => ['string', 'nullable', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'],
'docker_compose' => 'string|nullable',
'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable',
'is_container_label_escape_enabled' => 'boolean',
];
}
function validateIncomingRequest(Request $request)
{
// check if request is json
if (! $request->isJson()) {
return response()->json([
'message' => 'Invalid request.',
'error' => 'Content-Type must be application/json.',
], 400);
}
// check if request is valid json
if (! json_decode($request->getContent())) {
return response()->json([
'message' => 'Invalid request.',
'error' => 'Invalid JSON.',
], 400);
}
// check if valid json is empty
if (empty($request->json()->all())) {
return response()->json([
'message' => 'Invalid request.',
'error' => 'Empty JSON.',
], 400);
}
}
function removeUnnecessaryFieldsFromRequest(Request $request)
{
$request->offsetUnset('project_uuid');
$request->offsetUnset('environment_name');
$request->offsetUnset('environment_uuid');
$request->offsetUnset('destination_uuid');
$request->offsetUnset('server_uuid');
$request->offsetUnset('type');
$request->offsetUnset('domains');
$request->offsetUnset('instant_deploy');
$request->offsetUnset('github_app_uuid');
$request->offsetUnset('private_key_uuid');
$request->offsetUnset('use_build_server');
$request->offsetUnset('is_static');
$request->offsetUnset('is_spa');
$request->offsetUnset('is_auto_deploy_enabled');
$request->offsetUnset('is_force_https_enabled');
$request->offsetUnset('connect_to_docker_network');
$request->offsetUnset('force_domain_override');
$request->offsetUnset('autogenerate_domain');
$request->offsetUnset('is_container_label_escape_enabled');
$request->offsetUnset('docker_compose_raw');
}