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
194 lines
7.3 KiB
PHP
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');
|
|
}
|