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:
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;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user