Merge remote-tracking branch 'origin/next' into api-sensitive-data-scrubber

This commit is contained in:
Andras Bacsai committed 2026-07-07 12:56:19 +02:00
commit ff976a134f
136 files changed
+6142 -1733

No files matched your search

+1 -1
View File
@@ -218,7 +218,7 @@ describe('security UI audit logging', function () {
test('cloud provider token form does not contain debug ray calls', function () {
expect(file_get_contents(app_path('Livewire/Security/CloudProviderTokenForm.php')))
->not->toContain('ray(');
->not->toContain('ray'.'(');
});
});
@@ -130,6 +130,69 @@ describe('deployment job path field validation', function () {
});
describe('API validation rules for path fields', function () {
test('domains validation rejects command injection payloads', function (string $payload) {
$rules = sharedDataApplications();
$validator = validator(
['domains' => $payload],
['domains' => $rules['domains']]
);
expect($validator->fails())->toBeTrue();
})->with([
'host command substitution' => 'http://$(whoami).example.com',
'path command substitution' => 'http://example.com/$(whoami)',
'query command substitution' => 'http://example.com/path?next=$(id)',
'host backtick substitution' => 'http://`whoami`.example.com',
'path backtick substitution' => 'http://example.com/`whoami`',
'semicolon command separator' => 'http://example.com/path;id',
'newline injection' => "http://example.com\nwhoami",
'carriage return injection' => "http://example.com\rwhoami",
'pipe injection' => 'http://example.com/path|id',
]);
test('domains validation rejects non http schemes', function () {
$rules = sharedDataApplications();
$validator = validator(
['domains' => 'ftp://example.com'],
['domains' => $rules['domains']]
);
expect($validator->fails())->toBeTrue();
});
test('domains validation allows comma separated http and https urls', function () {
$rules = sharedDataApplications();
$validator = validator(
['domains' => 'https://app.example.com,http://api.example.com/path'],
['domains' => $rules['domains']]
);
expect($validator->fails())->toBeFalse();
});
test('docker compose service domains validation rejects command injection payloads', function () {
$rules = [
'docker_compose_domains' => 'array|nullable',
'docker_compose_domains.*' => 'array:name,domain',
'docker_compose_domains.*.name' => 'string|required',
'docker_compose_domains.*.domain' => ValidationPatterns::applicationDomainRules(),
];
$validator = validator(
[
'docker_compose_domains' => [
['name' => 'app', 'domain' => 'https://app.example.com/$(whoami)'],
],
],
$rules
);
expect($validator->fails())->toBeTrue();
});
test('git_branch validation rejects shell metacharacters', function (string $branch) {
$rules = sharedDataApplications();
@@ -276,7 +339,45 @@ describe('deployment git command escaping', function () {
expect($coolifyVariables->getValue($instance))
->toContain("COOLIFY_BRANCH='main`id`' ")
->toContain('COOLIFY_RESOURCE_UUID=app-uuid ');
->toContain("COOLIFY_RESOURCE_UUID='app-uuid' ");
});
test('coolify url and fqdn shell assignments are quoted', function () {
$job = new ReflectionClass(ApplicationDeploymentJob::class);
$instance = $job->newInstanceWithoutConstructor();
$application = new Application;
$application->uuid = 'app-uuid';
$application->git_branch = 'main';
$application->fqdn = 'https://app.example.com/path';
$application->compose_parsing_version = '3';
$settings = new ApplicationSetting;
$settings->include_source_commit_in_build = true;
$application->setRelation('settings', $settings);
foreach ([
'application' => $application,
'commit' => 'HEAD$(id)',
'pull_request_id' => 0,
] as $property => $value) {
$reflectionProperty = $job->getProperty($property);
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($instance, $value);
}
$method = $job->getMethod('set_coolify_variables');
$method->setAccessible(true);
$method->invoke($instance);
$coolifyVariables = $job->getProperty('coolify_variables');
$coolifyVariables->setAccessible(true);
expect($coolifyVariables->getValue($instance))
->toContain("SOURCE_COMMIT='HEAD$(id)' ")
->toContain("COOLIFY_URL='https://app.example.com/path' ")
->toContain("COOLIFY_FQDN='app.example.com' ")
->toContain("COOLIFY_RESOURCE_UUID='app-uuid' ");
});
});
@@ -44,7 +44,7 @@ function authenticateGithubSetupCallbackTest(object $test): void
session(['currentTeam' => $test->team]);
}
function fakeGithubManifestConversion(): void
function fakeGithubManifestConversion(string $apiUrl = 'https://api.github.com'): void
{
$key = openssl_pkey_new([
'private_key_bits' => 2048,
@@ -54,7 +54,7 @@ function fakeGithubManifestConversion(): void
Http::preventStrayRequests();
Http::fake([
'https://api.github.com/app-manifests/*/conversions' => Http::response([
"{$apiUrl}/app-manifests/*/conversions" => Http::response([
'id' => 987654,
'slug' => 'attacker-controlled-app',
'client_id' => 'new-client-id',
@@ -86,14 +86,14 @@ function configureGithubAppCredentials(GithubApp $githubApp): void
])->save();
}
function fakeGithubInstallationVerification(int $appId): void
function fakeGithubInstallationVerification(int $appId, string $apiUrl = 'https://api.github.com'): void
{
Http::preventStrayRequests();
Http::fake([
'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [
"{$apiUrl}/zen" => Http::response('Keep it logically awesome.', 200, [
'Date' => now()->toRfc7231String(),
]),
'https://api.github.com/app/installations/*' => Http::response([
"{$apiUrl}/app/installations/*" => Http::response([
'id' => 555,
'app_id' => $appId,
], 200),
@@ -183,6 +183,21 @@ it('configures an unbound github app with a valid one-time manifest state', func
->and($this->githubApp->private_key_id)->not->toBeNull();
});
it('converts ghe dot com app manifests through the data residency api host', function () {
authenticateGithubSetupCallbackTest($this);
$this->githubApp->forceFill([
'api_url' => 'https://api.octocorp.ghe.com',
'html_url' => 'https://octocorp.ghe.com',
])->save();
fakeGithubManifestConversion('https://api.octocorp.ghe.com');
cacheGithubAppSetupState('valid-state', 'manifest', $this->githubApp);
$this->get('/webhooks/source/github/redirect?state=valid-state&code=real-code')
->assertRedirect(route('source.github.show', ['github_app_uuid' => $this->githubApp->uuid]));
Http::assertSent(fn ($request) => $request->url() === 'https://api.octocorp.ghe.com/app-manifests/real-code/conversions');
});
it('rejects replayed github app manifest states', function () {
authenticateGithubSetupCallbackTest($this);
fakeGithubManifestConversion();
@@ -333,6 +348,23 @@ it('sets installation id when github confirms it belongs to the app', function (
expect($this->githubApp->installation_id)->toBe(123456);
});
it('verifies ghe dot com installations through the data residency api host', function () {
authenticateGithubSetupCallbackTest($this);
$this->githubApp->forceFill([
'api_url' => 'https://api.octocorp.ghe.com',
'html_url' => 'https://octocorp.ghe.com',
])->save();
configureGithubAppCredentials($this->githubApp);
fakeGithubInstallationVerification($this->githubApp->app_id, 'https://api.octocorp.ghe.com');
cacheGithubAppSetupState('valid-install-state', 'install', $this->githubApp);
$this->get('/webhooks/source/github/install?state=valid-install-state&setup_action=install&installation_id=123456')
->assertRedirect(route('source.github.show', ['github_app_uuid' => $this->githubApp->uuid]));
Http::assertSent(fn ($request) => $request->url() === 'https://api.octocorp.ghe.com/zen');
Http::assertSent(fn ($request) => $request->url() === 'https://api.octocorp.ghe.com/app/installations/123456');
});
it('rejects replayed github app install states', function () {
authenticateGithubSetupCallbackTest($this);
configureGithubAppCredentials($this->githubApp);