feat(validation): add custom validation rules for Git repository URLs and branches
- Introduced `ValidGitRepositoryUrl` and `ValidGitBranch` validation rules to ensure safe and valid input for Git repository URLs and branch names. - Updated relevant Livewire components and API controllers to utilize the new validation rules, enhancing security against command injection and invalid inputs. - Refactored existing validation logic to improve consistency and maintainability across the application.
This commit is contained in:
@@ -15,6 +15,8 @@ use App\Models\PrivateKey;
|
|||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
|
use App\Rules\ValidGitBranch;
|
||||||
|
use App\Rules\ValidGitRepositoryUrl;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
use OpenApi\Attributes as OA;
|
use OpenApi\Attributes as OA;
|
||||||
@@ -831,8 +833,8 @@ class ApplicationsController extends Controller
|
|||||||
$destination = $destinations->first();
|
$destination = $destinations->first();
|
||||||
if ($type === 'public') {
|
if ($type === 'public') {
|
||||||
$validationRules = [
|
$validationRules = [
|
||||||
'git_repository' => 'string|required',
|
'git_repository' => ['string', 'required', new ValidGitRepositoryUrl],
|
||||||
'git_branch' => 'string|required',
|
'git_branch' => ['string', 'required', new ValidGitBranch],
|
||||||
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
|
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
|
||||||
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
|
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
|
||||||
'docker_compose_location' => 'string',
|
'docker_compose_location' => 'string',
|
||||||
@@ -883,7 +885,7 @@ class ApplicationsController extends Controller
|
|||||||
$application->source_type = GithubApp::class;
|
$application->source_type = GithubApp::class;
|
||||||
$application->source_id = GithubApp::find(0)->id;
|
$application->source_id = GithubApp::find(0)->id;
|
||||||
}
|
}
|
||||||
$application->git_repository = $repository_url_parsed->getSegment(1).'/'.$repository_url_parsed->getSegment(2);
|
$application->git_repository = str($repository_url_parsed->getSegment(1).'/'.$repository_url_parsed->getSegment(2))->trim()->toString();
|
||||||
$application->fqdn = $fqdn;
|
$application->fqdn = $fqdn;
|
||||||
$application->destination_id = $destination->id;
|
$application->destination_id = $destination->id;
|
||||||
$application->destination_type = $destination->getMorphClass();
|
$application->destination_type = $destination->getMorphClass();
|
||||||
@@ -935,7 +937,7 @@ class ApplicationsController extends Controller
|
|||||||
} elseif ($type === 'private-gh-app') {
|
} elseif ($type === 'private-gh-app') {
|
||||||
$validationRules = [
|
$validationRules = [
|
||||||
'git_repository' => 'string|required',
|
'git_repository' => 'string|required',
|
||||||
'git_branch' => 'string|required',
|
'git_branch' => ['string', 'required', new ValidGitBranch],
|
||||||
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
|
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
|
||||||
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
|
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
|
||||||
'github_app_uuid' => 'string|required',
|
'github_app_uuid' => 'string|required',
|
||||||
@@ -1043,7 +1045,7 @@ class ApplicationsController extends Controller
|
|||||||
$application->docker_compose_domains = $dockerComposeDomainsJson;
|
$application->docker_compose_domains = $dockerComposeDomainsJson;
|
||||||
}
|
}
|
||||||
$application->fqdn = $fqdn;
|
$application->fqdn = $fqdn;
|
||||||
$application->git_repository = $gitRepository;
|
$application->git_repository = str($gitRepository)->trim()->toString();
|
||||||
$application->destination_id = $destination->id;
|
$application->destination_id = $destination->id;
|
||||||
$application->destination_type = $destination->getMorphClass();
|
$application->destination_type = $destination->getMorphClass();
|
||||||
$application->environment_id = $environment->id;
|
$application->environment_id = $environment->id;
|
||||||
@@ -1090,8 +1092,8 @@ class ApplicationsController extends Controller
|
|||||||
} elseif ($type === 'private-deploy-key') {
|
} elseif ($type === 'private-deploy-key') {
|
||||||
|
|
||||||
$validationRules = [
|
$validationRules = [
|
||||||
'git_repository' => 'string|required',
|
'git_repository' => ['string', 'required', new ValidGitRepositoryUrl],
|
||||||
'git_branch' => 'string|required',
|
'git_branch' => ['string', 'required', new ValidGitBranch],
|
||||||
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
|
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
|
||||||
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
|
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
|
||||||
'private_key_uuid' => 'string|required',
|
'private_key_uuid' => 'string|required',
|
||||||
|
@@ -7,6 +7,7 @@ use App\Models\GithubApp;
|
|||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Models\StandaloneDocker;
|
use App\Models\StandaloneDocker;
|
||||||
use App\Models\SwarmDocker;
|
use App\Models\SwarmDocker;
|
||||||
|
use App\Rules\ValidGitBranch;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
@@ -155,6 +156,21 @@ class GithubPrivateRepository extends Component
|
|||||||
public function submit()
|
public function submit()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
// Validate git repository parts and branch
|
||||||
|
$validator = validator([
|
||||||
|
'selected_repository_owner' => $this->selected_repository_owner,
|
||||||
|
'selected_repository_repo' => $this->selected_repository_repo,
|
||||||
|
'selected_branch_name' => $this->selected_branch_name,
|
||||||
|
], [
|
||||||
|
'selected_repository_owner' => 'required|string|regex:/^[a-zA-Z0-9\-_]+$/',
|
||||||
|
'selected_repository_repo' => 'required|string|regex:/^[a-zA-Z0-9\-_\.]+$/',
|
||||||
|
'selected_branch_name' => ['required', 'string', new ValidGitBranch],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
throw new \RuntimeException('Invalid repository data: '.$validator->errors()->first());
|
||||||
|
}
|
||||||
|
|
||||||
$destination_uuid = $this->query['destination'];
|
$destination_uuid = $this->query['destination'];
|
||||||
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
|
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
|
||||||
if (! $destination) {
|
if (! $destination) {
|
||||||
@@ -171,8 +187,8 @@ class GithubPrivateRepository extends Component
|
|||||||
$application = Application::create([
|
$application = Application::create([
|
||||||
'name' => generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name),
|
'name' => generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name),
|
||||||
'repository_project_id' => $this->selected_repository_id,
|
'repository_project_id' => $this->selected_repository_id,
|
||||||
'git_repository' => "{$this->selected_repository_owner}/{$this->selected_repository_repo}",
|
'git_repository' => str($this->selected_repository_owner)->trim()->toString().'/'.str($this->selected_repository_repo)->trim()->toString(),
|
||||||
'git_branch' => $this->selected_branch_name,
|
'git_branch' => str($this->selected_branch_name)->trim()->toString(),
|
||||||
'build_pack' => $this->build_pack,
|
'build_pack' => $this->build_pack,
|
||||||
'ports_exposes' => $this->port,
|
'ports_exposes' => $this->port,
|
||||||
'publish_directory' => $this->publish_directory,
|
'publish_directory' => $this->publish_directory,
|
||||||
|
@@ -9,6 +9,8 @@ use App\Models\PrivateKey;
|
|||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Models\StandaloneDocker;
|
use App\Models\StandaloneDocker;
|
||||||
use App\Models\SwarmDocker;
|
use App\Models\SwarmDocker;
|
||||||
|
use App\Rules\ValidGitBranch;
|
||||||
|
use App\Rules\ValidGitRepositoryUrl;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Spatie\Url\Url;
|
use Spatie\Url\Url;
|
||||||
@@ -53,17 +55,29 @@ class GithubPrivateRepositoryDeployKey extends Component
|
|||||||
|
|
||||||
private ?string $git_host = null;
|
private ?string $git_host = null;
|
||||||
|
|
||||||
private string $git_repository;
|
private ?string $git_repository = null;
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'repository_url' => 'required',
|
'repository_url' => ['required', 'string'],
|
||||||
'branch' => 'required|string',
|
'branch' => ['required', 'string'],
|
||||||
'port' => 'required|numeric',
|
'port' => 'required|numeric',
|
||||||
'is_static' => 'required|boolean',
|
'is_static' => 'required|boolean',
|
||||||
'publish_directory' => 'nullable|string',
|
'publish_directory' => 'nullable|string',
|
||||||
'build_pack' => 'required|string',
|
'build_pack' => 'required|string',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected function rules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'repository_url' => ['required', 'string', new ValidGitRepositoryUrl],
|
||||||
|
'branch' => ['required', 'string', new ValidGitBranch],
|
||||||
|
'port' => 'required|numeric',
|
||||||
|
'is_static' => 'required|boolean',
|
||||||
|
'publish_directory' => 'nullable|string',
|
||||||
|
'build_pack' => 'required|string',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected $validationAttributes = [
|
protected $validationAttributes = [
|
||||||
'repository_url' => 'Repository',
|
'repository_url' => 'Repository',
|
||||||
'branch' => 'Branch',
|
'branch' => 'Branch',
|
||||||
@@ -135,6 +149,9 @@ class GithubPrivateRepositoryDeployKey extends Component
|
|||||||
|
|
||||||
$this->get_git_source();
|
$this->get_git_source();
|
||||||
|
|
||||||
|
// Note: git_repository has already been validated and transformed in get_git_source()
|
||||||
|
// It may now be in SSH format (git@host:repo.git) which is valid for deploy keys
|
||||||
|
|
||||||
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
|
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
|
||||||
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
|
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
|
||||||
if ($this->git_source === 'other') {
|
if ($this->git_source === 'other') {
|
||||||
@@ -194,6 +211,15 @@ class GithubPrivateRepositoryDeployKey extends Component
|
|||||||
|
|
||||||
private function get_git_source()
|
private function get_git_source()
|
||||||
{
|
{
|
||||||
|
// Validate repository URL before parsing
|
||||||
|
$validator = validator(['repository_url' => $this->repository_url], [
|
||||||
|
'repository_url' => ['required', 'string', new ValidGitRepositoryUrl],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
throw new \RuntimeException('Invalid repository URL: '.$validator->errors()->first('repository_url'));
|
||||||
|
}
|
||||||
|
|
||||||
$this->repository_url_parsed = Url::fromString($this->repository_url);
|
$this->repository_url_parsed = Url::fromString($this->repository_url);
|
||||||
$this->git_host = $this->repository_url_parsed->getHost();
|
$this->git_host = $this->repository_url_parsed->getHost();
|
||||||
$this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2);
|
$this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2);
|
||||||
@@ -206,8 +232,10 @@ class GithubPrivateRepositoryDeployKey extends Component
|
|||||||
if (str($this->repository_url)->startsWith('http')) {
|
if (str($this->repository_url)->startsWith('http')) {
|
||||||
$this->git_host = $this->repository_url_parsed->getHost();
|
$this->git_host = $this->repository_url_parsed->getHost();
|
||||||
$this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2);
|
$this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2);
|
||||||
|
// Convert to SSH format for deploy key usage
|
||||||
$this->git_repository = Str::finish("git@$this->git_host:$this->git_repository", '.git');
|
$this->git_repository = Str::finish("git@$this->git_host:$this->git_repository", '.git');
|
||||||
} else {
|
} else {
|
||||||
|
// If it's already in SSH format, just use it as-is
|
||||||
$this->git_repository = $this->repository_url;
|
$this->git_repository = $this->repository_url;
|
||||||
}
|
}
|
||||||
$this->git_source = 'other';
|
$this->git_source = 'other';
|
||||||
|
@@ -9,6 +9,8 @@ use App\Models\Project;
|
|||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use App\Models\StandaloneDocker;
|
use App\Models\StandaloneDocker;
|
||||||
use App\Models\SwarmDocker;
|
use App\Models\SwarmDocker;
|
||||||
|
use App\Rules\ValidGitBranch;
|
||||||
|
use App\Rules\ValidGitRepositoryUrl;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Spatie\Url\Url;
|
use Spatie\Url\Url;
|
||||||
@@ -62,7 +64,7 @@ class PublicGitRepository extends Component
|
|||||||
public bool $new_compose_services = false;
|
public bool $new_compose_services = false;
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'repository_url' => 'required|url',
|
'repository_url' => ['required', 'string'],
|
||||||
'port' => 'required|numeric',
|
'port' => 'required|numeric',
|
||||||
'isStatic' => 'required|boolean',
|
'isStatic' => 'required|boolean',
|
||||||
'publish_directory' => 'nullable|string',
|
'publish_directory' => 'nullable|string',
|
||||||
@@ -71,6 +73,20 @@ class PublicGitRepository extends Component
|
|||||||
'docker_compose_location' => 'nullable|string',
|
'docker_compose_location' => 'nullable|string',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected function rules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'repository_url' => ['required', 'string', new ValidGitRepositoryUrl],
|
||||||
|
'port' => 'required|numeric',
|
||||||
|
'isStatic' => 'required|boolean',
|
||||||
|
'publish_directory' => 'nullable|string',
|
||||||
|
'build_pack' => 'required|string',
|
||||||
|
'base_directory' => 'nullable|string',
|
||||||
|
'docker_compose_location' => 'nullable|string',
|
||||||
|
'git_branch' => ['required', 'string', new ValidGitBranch],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected $validationAttributes = [
|
protected $validationAttributes = [
|
||||||
'repository_url' => 'repository',
|
'repository_url' => 'repository',
|
||||||
'port' => 'port',
|
'port' => 'port',
|
||||||
@@ -141,6 +157,15 @@ class PublicGitRepository extends Component
|
|||||||
public function loadBranch()
|
public function loadBranch()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
// Validate repository URL
|
||||||
|
$validator = validator(['repository_url' => $this->repository_url], [
|
||||||
|
'repository_url' => ['required', 'string', new ValidGitRepositoryUrl],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
throw new \RuntimeException('Invalid repository URL: '.$validator->errors()->first('repository_url'));
|
||||||
|
}
|
||||||
|
|
||||||
if (str($this->repository_url)->startsWith('git@')) {
|
if (str($this->repository_url)->startsWith('git@')) {
|
||||||
$github_instance = str($this->repository_url)->after('git@')->before(':');
|
$github_instance = str($this->repository_url)->after('git@')->before(':');
|
||||||
$repository = str($this->repository_url)->after(':')->before('.git');
|
$repository = str($this->repository_url)->after(':')->before('.git');
|
||||||
@@ -191,6 +216,15 @@ class PublicGitRepository extends Component
|
|||||||
$this->git_branch = 'main';
|
$this->git_branch = 'main';
|
||||||
$this->base_directory = '/';
|
$this->base_directory = '/';
|
||||||
|
|
||||||
|
// Validate repository URL before parsing
|
||||||
|
$validator = validator(['repository_url' => $this->repository_url], [
|
||||||
|
'repository_url' => ['required', 'string', new ValidGitRepositoryUrl],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
throw new \RuntimeException('Invalid repository URL: '.$validator->errors()->first('repository_url'));
|
||||||
|
}
|
||||||
|
|
||||||
$this->repository_url_parsed = Url::fromString($this->repository_url);
|
$this->repository_url_parsed = Url::fromString($this->repository_url);
|
||||||
$this->git_host = $this->repository_url_parsed->getHost();
|
$this->git_host = $this->repository_url_parsed->getHost();
|
||||||
$this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2);
|
$this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2);
|
||||||
@@ -234,6 +268,27 @@ class PublicGitRepository extends Component
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->validate();
|
$this->validate();
|
||||||
|
|
||||||
|
// Additional validation for git repository and branch
|
||||||
|
if ($this->git_source === 'other') {
|
||||||
|
// For 'other' sources, git_repository contains the full URL
|
||||||
|
$validator = validator(['git_repository' => $this->git_repository], [
|
||||||
|
'git_repository' => ['required', 'string', new ValidGitRepositoryUrl],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
throw new \RuntimeException('Invalid repository URL: '.$validator->errors()->first('git_repository'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$branchValidator = validator(['git_branch' => $this->git_branch], [
|
||||||
|
'git_branch' => ['required', 'string', new ValidGitBranch],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($branchValidator->fails()) {
|
||||||
|
throw new \RuntimeException('Invalid branch: '.$branchValidator->errors()->first('git_branch'));
|
||||||
|
}
|
||||||
|
|
||||||
$destination_uuid = $this->query['destination'];
|
$destination_uuid = $this->query['destination'];
|
||||||
$project_uuid = $this->parameters['project_uuid'];
|
$project_uuid = $this->parameters['project_uuid'];
|
||||||
$environment_uuid = $this->parameters['environment_uuid'];
|
$environment_uuid = $this->parameters['environment_uuid'];
|
||||||
|
@@ -1128,15 +1128,20 @@ class Application extends BaseModel
|
|||||||
$branch = $this->git_branch;
|
$branch = $this->git_branch;
|
||||||
['repository' => $customRepository, 'port' => $customPort] = $this->customRepository();
|
['repository' => $customRepository, 'port' => $customPort] = $this->customRepository();
|
||||||
$baseDir = $custom_base_dir ?? $this->generateBaseDir($deployment_uuid);
|
$baseDir = $custom_base_dir ?? $this->generateBaseDir($deployment_uuid);
|
||||||
|
|
||||||
|
// Escape shell arguments for safety to prevent command injection
|
||||||
|
$escapedBranch = escapeshellarg($branch);
|
||||||
|
$escapedBaseDir = escapeshellarg($baseDir);
|
||||||
|
|
||||||
$commands = collect([]);
|
$commands = collect([]);
|
||||||
|
|
||||||
// Check if shallow clone is enabled
|
// Check if shallow clone is enabled
|
||||||
$isShallowCloneEnabled = $this->settings?->is_git_shallow_clone_enabled ?? false;
|
$isShallowCloneEnabled = $this->settings?->is_git_shallow_clone_enabled ?? false;
|
||||||
$depthFlag = $isShallowCloneEnabled ? ' --depth=1' : '';
|
$depthFlag = $isShallowCloneEnabled ? ' --depth=1' : '';
|
||||||
|
|
||||||
$git_clone_command = "git clone{$depthFlag} -b \"{$this->git_branch}\"";
|
$git_clone_command = "git clone{$depthFlag} -b {$escapedBranch}";
|
||||||
if ($only_checkout) {
|
if ($only_checkout) {
|
||||||
$git_clone_command = "git clone{$depthFlag} --no-checkout -b \"{$this->git_branch}\"";
|
$git_clone_command = "git clone{$depthFlag} --no-checkout -b {$escapedBranch}";
|
||||||
}
|
}
|
||||||
if ($pull_request_id !== 0) {
|
if ($pull_request_id !== 0) {
|
||||||
$pr_branch_name = "pr-{$pull_request_id}-coolify";
|
$pr_branch_name = "pr-{$pull_request_id}-coolify";
|
||||||
@@ -1150,7 +1155,8 @@ class Application extends BaseModel
|
|||||||
if ($this->source->getMorphClass() === \App\Models\GithubApp::class) {
|
if ($this->source->getMorphClass() === \App\Models\GithubApp::class) {
|
||||||
if ($this->source->is_public) {
|
if ($this->source->is_public) {
|
||||||
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
|
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
|
||||||
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}";
|
$escapedRepoUrl = escapeshellarg("{$this->source->html_url}/{$customRepository}");
|
||||||
|
$git_clone_command = "{$git_clone_command} {$escapedRepoUrl} {$escapedBaseDir}";
|
||||||
if (! $only_checkout) {
|
if (! $only_checkout) {
|
||||||
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true);
|
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true);
|
||||||
}
|
}
|
||||||
@@ -1162,11 +1168,15 @@ class Application extends BaseModel
|
|||||||
} else {
|
} else {
|
||||||
$github_access_token = generateGithubInstallationToken($this->source);
|
$github_access_token = generateGithubInstallationToken($this->source);
|
||||||
if ($exec_in_docker) {
|
if ($exec_in_docker) {
|
||||||
$git_clone_command = "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git {$baseDir}";
|
$repoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
|
||||||
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
|
$escapedRepoUrl = escapeshellarg($repoUrl);
|
||||||
|
$git_clone_command = "{$git_clone_command} {$escapedRepoUrl} {$escapedBaseDir}";
|
||||||
|
$fullRepoUrl = $repoUrl;
|
||||||
} else {
|
} else {
|
||||||
$git_clone_command = "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository} {$baseDir}";
|
$repoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
|
||||||
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
|
$escapedRepoUrl = escapeshellarg($repoUrl);
|
||||||
|
$git_clone_command = "{$git_clone_command} {$escapedRepoUrl} {$escapedBaseDir}";
|
||||||
|
$fullRepoUrl = $repoUrl;
|
||||||
}
|
}
|
||||||
if (! $only_checkout) {
|
if (! $only_checkout) {
|
||||||
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: false);
|
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: false);
|
||||||
@@ -1181,10 +1191,11 @@ class Application extends BaseModel
|
|||||||
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
|
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
|
||||||
|
|
||||||
$git_checkout_command = $this->buildGitCheckoutCommand($pr_branch_name);
|
$git_checkout_command = $this->buildGitCheckoutCommand($pr_branch_name);
|
||||||
|
$escapedPrBranch = escapeshellarg($branch);
|
||||||
if ($exec_in_docker) {
|
if ($exec_in_docker) {
|
||||||
$commands->push(executeInDocker($deployment_uuid, "cd {$baseDir} && git fetch origin {$branch} && $git_checkout_command"));
|
$commands->push(executeInDocker($deployment_uuid, "cd {$escapedBaseDir} && git fetch origin {$escapedPrBranch} && $git_checkout_command"));
|
||||||
} else {
|
} else {
|
||||||
$commands->push("cd {$baseDir} && git fetch origin {$branch} && $git_checkout_command");
|
$commands->push("cd {$escapedBaseDir} && git fetch origin {$escapedPrBranch} && $git_checkout_command");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1202,7 +1213,8 @@ class Application extends BaseModel
|
|||||||
throw new RuntimeException('Private key not found. Please add a private key to the application and try again.');
|
throw new RuntimeException('Private key not found. Please add a private key to the application and try again.');
|
||||||
}
|
}
|
||||||
$private_key = base64_encode($private_key);
|
$private_key = base64_encode($private_key);
|
||||||
$git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$customRepository} {$baseDir}";
|
$escapedCustomRepository = escapeshellarg($customRepository);
|
||||||
|
$git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}";
|
||||||
if ($only_checkout) {
|
if ($only_checkout) {
|
||||||
$git_clone_command = $git_clone_command_base;
|
$git_clone_command = $git_clone_command_base;
|
||||||
} else {
|
} else {
|
||||||
|
96
app/Rules/ValidGitBranch.php
Normal file
96
app/Rules/ValidGitBranch.php
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Rules;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class ValidGitBranch implements ValidationRule
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the validation rule.
|
||||||
|
*/
|
||||||
|
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||||
|
{
|
||||||
|
if (empty($value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$branch = trim($value);
|
||||||
|
|
||||||
|
// Check for dangerous shell metacharacters
|
||||||
|
$dangerousChars = [
|
||||||
|
';', '|', '&', '$', '`', '(', ')', '{', '}',
|
||||||
|
'<', '>', '\n', '\r', '\0', '"', "'", '\\',
|
||||||
|
'!', '*', '?', '[', ']', '~', '^', ':', ' ',
|
||||||
|
'#',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($dangerousChars as $char) {
|
||||||
|
if (str_contains($branch, $char)) {
|
||||||
|
Log::warning('Git branch validation failed - dangerous character', [
|
||||||
|
'branch' => $branch,
|
||||||
|
'character' => $char,
|
||||||
|
'ip' => request()->ip(),
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
]);
|
||||||
|
$fail('The :attribute contains invalid characters.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Git branch name rules:
|
||||||
|
// - Cannot contain: .., //, @{
|
||||||
|
// - Cannot start or end with: / or .
|
||||||
|
// - Cannot be empty after trimming
|
||||||
|
|
||||||
|
if (str_contains($branch, '..') ||
|
||||||
|
str_contains($branch, '//') ||
|
||||||
|
str_contains($branch, '@{')) {
|
||||||
|
$fail('The :attribute contains invalid patterns.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($branch, '/') ||
|
||||||
|
str_ends_with($branch, '/') ||
|
||||||
|
str_starts_with($branch, '.') ||
|
||||||
|
str_ends_with($branch, '.')) {
|
||||||
|
$fail('The :attribute cannot start or end with / or .');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow only safe characters for branch names
|
||||||
|
// Letters, numbers, hyphens, underscores, forward slashes, and dots
|
||||||
|
if (! preg_match('/^[a-zA-Z0-9\-_\/\.]+$/', $branch)) {
|
||||||
|
$fail('The :attribute contains invalid characters. Only letters, numbers, hyphens, underscores, forward slashes, and dots are allowed.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional Git-specific validations
|
||||||
|
// Branch name cannot be 'HEAD'
|
||||||
|
if ($branch === 'HEAD') {
|
||||||
|
$fail('The :attribute cannot be HEAD.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for consecutive dots (not allowed in Git)
|
||||||
|
if (str_contains($branch, '..')) {
|
||||||
|
$fail('The :attribute cannot contain consecutive dots.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for .lock suffix (reserved by Git)
|
||||||
|
if (str_ends_with($branch, '.lock')) {
|
||||||
|
$fail('The :attribute cannot end with .lock.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
157
app/Rules/ValidGitRepositoryUrl.php
Normal file
157
app/Rules/ValidGitRepositoryUrl.php
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Rules;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class ValidGitRepositoryUrl implements ValidationRule
|
||||||
|
{
|
||||||
|
protected bool $allowSSH;
|
||||||
|
|
||||||
|
protected bool $allowIP;
|
||||||
|
|
||||||
|
public function __construct(bool $allowSSH = true, bool $allowIP = false)
|
||||||
|
{
|
||||||
|
$this->allowSSH = $allowSSH;
|
||||||
|
$this->allowIP = $allowIP;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the validation rule.
|
||||||
|
*/
|
||||||
|
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||||
|
{
|
||||||
|
if (empty($value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for dangerous shell metacharacters that could be used for command injection
|
||||||
|
$dangerousChars = [
|
||||||
|
';', '|', '&', '$', '`', '(', ')', '{', '}',
|
||||||
|
'[', ']', '<', '>', '\n', '\r', '\0', '"', "'",
|
||||||
|
'\\', '!', '?', '*', '~', '^', '%', '=', '+',
|
||||||
|
'#', // Comment character that could hide commands
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($dangerousChars as $char) {
|
||||||
|
if (str_contains($value, $char)) {
|
||||||
|
Log::warning('Git repository URL validation failed - dangerous character', [
|
||||||
|
'url' => $value,
|
||||||
|
'character' => $char,
|
||||||
|
'ip' => request()->ip(),
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
]);
|
||||||
|
$fail('The :attribute contains invalid characters.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for command substitution patterns
|
||||||
|
$dangerousPatterns = [
|
||||||
|
'/\$\(.*\)/', // Command substitution $(...)
|
||||||
|
'/\${.*}/', // Variable expansion ${...}
|
||||||
|
'/;;/', // Double semicolon
|
||||||
|
'/&&/', // Command chaining
|
||||||
|
'/\|\|/', // Command chaining
|
||||||
|
'/>>/', // Redirect append
|
||||||
|
'/<</', // Here document
|
||||||
|
'/\\\n/', // Line continuation
|
||||||
|
'/\.\.[\/\\\\]/', // Directory traversal
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($dangerousPatterns as $pattern) {
|
||||||
|
if (preg_match($pattern, $value)) {
|
||||||
|
Log::warning('Git repository URL validation failed - dangerous pattern', [
|
||||||
|
'url' => $value,
|
||||||
|
'pattern' => $pattern,
|
||||||
|
'ip' => request()->ip(),
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
]);
|
||||||
|
$fail('The :attribute contains invalid patterns.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate based on URL type
|
||||||
|
if (str_starts_with($value, 'git@')) {
|
||||||
|
if (! $this->allowSSH) {
|
||||||
|
$fail('SSH URLs are not allowed.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate SSH URL format (git@host:user/repo.git)
|
||||||
|
if (! preg_match('/^git@[a-zA-Z0-9\.\-]+:[a-zA-Z0-9\-_\/\.]+$/', $value)) {
|
||||||
|
$fail('The :attribute is not a valid SSH repository URL.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} elseif (str_starts_with($value, 'http://') || str_starts_with($value, 'https://')) {
|
||||||
|
// Validate HTTP(S) URL
|
||||||
|
if (! filter_var($value, FILTER_VALIDATE_URL)) {
|
||||||
|
$fail('The :attribute is not a valid URL.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parsed = parse_url($value);
|
||||||
|
|
||||||
|
// Check for IP addresses if not allowed
|
||||||
|
if (! $this->allowIP && filter_var($parsed['host'] ?? '', FILTER_VALIDATE_IP)) {
|
||||||
|
Log::warning('Git repository URL contains IP address', [
|
||||||
|
'url' => $value,
|
||||||
|
'ip' => request()->ip(),
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
]);
|
||||||
|
$fail('The :attribute cannot use IP addresses.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for localhost/internal addresses
|
||||||
|
$host = strtolower($parsed['host'] ?? '');
|
||||||
|
$internalHosts = ['localhost', '127.0.0.1', '0.0.0.0', '::1'];
|
||||||
|
if (in_array($host, $internalHosts) || str_ends_with($host, '.local')) {
|
||||||
|
Log::warning('Git repository URL points to internal host', [
|
||||||
|
'url' => $value,
|
||||||
|
'host' => $host,
|
||||||
|
'ip' => request()->ip(),
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
]);
|
||||||
|
$fail('The :attribute cannot point to internal hosts.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure no query parameters or fragments
|
||||||
|
if (! empty($parsed['query']) || ! empty($parsed['fragment'])) {
|
||||||
|
$fail('The :attribute should not contain query parameters or fragments.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate path contains only safe characters
|
||||||
|
$path = $parsed['path'] ?? '';
|
||||||
|
if (! empty($path) && ! preg_match('/^[a-zA-Z0-9\-_\/\.]+$/', $path)) {
|
||||||
|
$fail('The :attribute path contains invalid characters.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} elseif (str_starts_with($value, 'git://')) {
|
||||||
|
// Validate git:// protocol URL
|
||||||
|
if (! preg_match('/^git:\/\/[a-zA-Z0-9\.\-]+\/[a-zA-Z0-9\-_\/\.]+$/', $value)) {
|
||||||
|
$fail('The :attribute is not a valid git:// URL.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$fail('The :attribute must start with https://, http://, git://, or git@.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h1>Create a new Application</h1>
|
<h1>Create a new Application</h1>
|
||||||
<div class="pb-4">Deploy any public or private Git repositories through a Deploy Key.</div>
|
<div class="pb-4">Deploy any public or private Git repositories through a Deploy Key.</div>
|
||||||
<div class="flex flex-col pt-4">
|
<div class="flex flex-col ">
|
||||||
@if ($current_step === 'private_keys')
|
@if ($current_step === 'private_keys')
|
||||||
<h2 class="pb-4">Select a private key</h2>
|
<h2 class="pb-4">Select a private key</h2>
|
||||||
<div class="flex flex-col justify-center gap-2 text-left ">
|
<div class="flex flex-col justify-center gap-2 text-left ">
|
||||||
@@ -46,9 +46,8 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@if ($current_step === 'repository')
|
@if ($current_step === 'repository')
|
||||||
<h2 class="pb-4">Select a repository</h2>
|
<form class="flex flex-col gap-2" wire:submit='submit'>
|
||||||
<form class="flex flex-col gap-2 pt-2" wire:submit='submit'>
|
<x-forms.input id="repository_url" required label="Repository URL (https:// or git@)" />
|
||||||
<x-forms.input id="repository_url" required label="Repository Url (https:// or git@)" />
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input id="branch" required label="Branch" />
|
<x-forms.input id="branch" required label="Branch" />
|
||||||
<x-forms.select wire:model.live="build_pack" label="Build Pack" required>
|
<x-forms.select wire:model.live="build_pack" label="Build Pack" required>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<div x-data x-init="$nextTick(() => { if ($refs.autofocusInput) $refs.autofocusInput.focus(); })">
|
<div x-data x-init="$nextTick(() => { if ($refs.autofocusInput) $refs.autofocusInput.focus(); })">
|
||||||
<h1>Create a new Application</h1>
|
<h1>Create a new Application</h1>
|
||||||
<div class="pb-4">Deploy any public Git repositories.</div>
|
<div class="pb-8">Deploy any public Git repositories.</div>
|
||||||
|
|
||||||
<!-- Repository URL Form -->
|
<!-- Repository URL Form -->
|
||||||
<form class="flex flex-col gap-2" wire:submit='loadBranch'>
|
<form class="flex flex-col gap-2" wire:submit='loadBranch'>
|
||||||
|
Reference in New Issue
Block a user