mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-28 09:34:36 +08:00
Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Furox <177975472+Furox-Art@users.noreply.github.com> Co-authored-by: johandenhollander <johan.den.hollander@finalist.nl> Co-authored-by: João Cruz <161638790+SrJooJ@users.noreply.github.com> Co-authored-by: Saim Hashmi <hashmisaim037@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
182 lines
6.8 KiB
PHP
182 lines
6.8 KiB
PHP
<?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 = true)
|
|
{
|
|
$this->allowSSH = $allowSSH;
|
|
$this->allowIP = $allowIP;
|
|
}
|
|
|
|
/**
|
|
* Run the validation rule.
|
|
*/
|
|
public function validate(string $attribute, mixed $value, Closure $fail): void
|
|
{
|
|
if (empty($value)) {
|
|
return;
|
|
}
|
|
|
|
// Brackets are only allowed around an IPv6 host, so check the rest of the URL without it.
|
|
$valueWithoutIpv6Host = preg_replace_callback(
|
|
'/^((?:https?|git):\/\/|(?:ssh:\/\/)?[a-zA-Z0-9._-]+@)\[([0-9A-Fa-f:.]+)\]/',
|
|
fn (array $matches): string => filter_var($matches[2], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false ? $matches[1].'ipv6-host' : $matches[0],
|
|
$value,
|
|
);
|
|
|
|
// 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($valueWithoutIpv6Host, $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, $valueWithoutIpv6Host)) {
|
|
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 (preg_match('/^[a-zA-Z0-9._-]+@(\[[0-9A-Fa-f:.]+\]|[a-zA-Z0-9.-]+):/', $value)) {
|
|
if (! $this->allowSSH) {
|
|
$fail('SSH URLs are not allowed.');
|
|
|
|
return;
|
|
}
|
|
|
|
// Validate scp-style SSH URL format (user@host:user/repo.git)
|
|
$scp = parseScpStyleGitUrl($value);
|
|
if ($scp === null || preg_match('/^(\[[0-9A-Fa-f:.]+\]|[a-zA-Z0-9.-]+)$/', $scp['host']) !== 1 || preg_match('/^[a-zA-Z0-9\-_\/.~]+$/', $scp['path']) !== 1) {
|
|
$fail('The :attribute is not a valid SSH repository URL.');
|
|
|
|
return;
|
|
}
|
|
} elseif (str_starts_with($value, 'ssh://')) {
|
|
if (! $this->allowSSH) {
|
|
$fail('SSH URLs are not allowed.');
|
|
|
|
return;
|
|
}
|
|
|
|
// Validate ssh:// URL format (ssh://user@host[:port]/user/repo.git)
|
|
if (! preg_match('/^ssh:\/\/[a-zA-Z0-9._-]+@(\[[0-9A-Fa-f:.]+\]|[a-zA-Z0-9.-]+)(:[0-9]{1,5})?\/[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);
|
|
// parse_url() keeps the brackets around an IPv6 host.
|
|
$host = trim(strtolower($parsed['host'] ?? ''), '[]');
|
|
|
|
// Check for IP addresses if not allowed
|
|
if (! $this->allowIP && filter_var($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
|
|
$internalHosts = ['localhost', '127.0.0.1', '0.0.0.0', '::1'];
|
|
$isReservedIp = filter_var($host, FILTER_VALIDATE_IP) !== false
|
|
&& filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE) === false;
|
|
if (in_array($host, $internalHosts) || $isReservedIp || 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 (supports both git://host/path and git://host:port/path with tilde)
|
|
if (! preg_match('/^git:\/\/(\[[0-9A-Fa-f:.]+\]|[a-zA-Z0-9\.\-]+)(:[0-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://, ssh://, or be an SSH URL (user@host:path).');
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|