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

This commit is contained in:
Andras Bacsai
2026-07-02 15:57:43 +02:00
117 changed files with 5114 additions and 1799 deletions
@@ -2943,8 +2943,12 @@ class ApplicationsController extends Controller
$this->authorize('manageEnvironment', $application);
if ($request->has('key')) {
$request->merge(['key' => ValidationPatterns::normalizeEnvironmentVariableKey((string) $request->key)]);
}
$validator = customApiValidator($request->all(), [
'key' => 'string|required',
'key' => ValidationPatterns::environmentVariableKeyRules(),
'value' => 'string|nullable',
'is_preview' => 'boolean',
'is_literal' => 'boolean',
@@ -3167,12 +3171,18 @@ class ApplicationsController extends Controller
], 400);
}
$bulk_data = collect($bulk_data)->map(function ($item) {
return collect($item)->only(['key', 'value', 'is_preview', 'is_literal', 'is_multiline', 'is_shown_once', 'is_runtime', 'is_buildtime', 'comment']);
$item = collect($item)->only(['key', 'value', 'is_preview', 'is_literal', 'is_multiline', 'is_shown_once', 'is_runtime', 'is_buildtime', 'comment']);
if ($item->has('key')) {
$item->put('key', ValidationPatterns::normalizeEnvironmentVariableKey((string) $item->get('key')));
}
return $item;
});
$returnedEnvs = collect();
foreach ($bulk_data as $item) {
$validator = customApiValidator($item, [
'key' => 'string|required',
'key' => ValidationPatterns::environmentVariableKeyRules(),
'value' => 'string|nullable',
'is_preview' => 'boolean',
'is_literal' => 'boolean',
@@ -3369,8 +3379,12 @@ class ApplicationsController extends Controller
$this->authorize('manageEnvironment', $application);
if ($request->has('key')) {
$request->merge(['key' => ValidationPatterns::normalizeEnvironmentVariableKey((string) $request->key)]);
}
$validator = customApiValidator($request->all(), [
'key' => 'string|required',
'key' => ValidationPatterns::environmentVariableKeyRules(),
'value' => 'string|nullable',
'is_preview' => 'boolean',
'is_literal' => 'boolean',
@@ -4333,10 +4347,11 @@ class ApplicationsController extends Controller
'host_path' => ['string', 'nullable', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN],
'content' => 'string|nullable',
'is_directory' => 'boolean',
'is_host_file' => 'boolean',
'fs_path' => 'string',
]);
$allAllowedFields = ['type', 'name', 'mount_path', 'host_path', 'content', 'is_directory', 'fs_path'];
$allAllowedFields = ['type', 'name', 'mount_path', 'host_path', 'content', 'is_directory', 'is_host_file', 'fs_path'];
$extraFields = array_diff(array_keys($request->all()), $allAllowedFields);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
@@ -4360,7 +4375,7 @@ class ApplicationsController extends Controller
], 422);
}
$typeSpecificInvalidFields = array_intersect(['content', 'is_directory', 'fs_path'], array_keys($request->all()));
$typeSpecificInvalidFields = array_intersect(['content', 'is_directory', 'is_host_file', 'fs_path'], array_keys($request->all()));
if (! empty($typeSpecificInvalidFields)) {
return response()->json([
'message' => 'Validation failed.',
@@ -4391,6 +4406,14 @@ class ApplicationsController extends Controller
}
$isDirectory = $request->boolean('is_directory', false);
$isHostFile = $request->boolean('is_host_file', false);
if ($isDirectory && $isHostFile) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['is_host_file' => 'Host file mounts cannot also be directory mounts.'],
], 422);
}
if ($isDirectory) {
if (! $request->fs_path) {
@@ -4413,12 +4436,50 @@ class ApplicationsController extends Controller
'resource_id' => $application->id,
'resource_type' => get_class($application),
]);
} elseif ($isHostFile) {
if (! $request->fs_path) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['fs_path' => 'The fs_path field is required for host file mounts.'],
], 422);
}
if ($request->filled('content')) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['content' => 'Content is not valid for host file mounts.'],
], 422);
}
try {
$fsPath = validateHostFileMountPath($request->fs_path, 'host file source path');
$mountPath = validateFileMountPath($request->mount_path, 'host file destination path');
} catch (\Throwable $e) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['mount_path' => $e->getMessage()],
], 422);
}
$storage = LocalFileVolume::create([
'fs_path' => $fsPath,
'mount_path' => $mountPath,
'content' => null,
'is_directory' => false,
'is_host_file' => true,
'resource_id' => $application->id,
'resource_type' => get_class($application),
]);
} else {
$mountPath = str($request->mount_path)->trim()->start('/')->value();
validateShellSafePath($mountPath, 'file storage path');
$fsPath = application_configuration_dir().'/'.$application->uuid.$mountPath;
try {
$mountPath = validateFileMountPath($request->mount_path, 'file storage path');
$fsPath = confineFileMountPath(application_configuration_dir().'/'.$application->uuid, $mountPath, 'file storage path');
} catch (\Throwable $e) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['mount_path' => $e->getMessage()],
], 422);
}
$storage = LocalFileVolume::create([
'fs_path' => $fsPath,
@@ -3207,8 +3207,12 @@ class DatabasesController extends Controller
$this->authorize('manageEnvironment', $database);
if ($request->has('key')) {
$request->merge(['key' => ValidationPatterns::normalizeEnvironmentVariableKey((string) $request->key)]);
}
$validator = customApiValidator($request->all(), [
'key' => 'string|required',
'key' => ValidationPatterns::environmentVariableKeyRules(),
'value' => 'string|nullable',
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
@@ -3355,8 +3359,12 @@ class DatabasesController extends Controller
$updatedEnvs = collect();
foreach ($bulk_data as $item) {
if (array_key_exists('key', $item)) {
$item['key'] = ValidationPatterns::normalizeEnvironmentVariableKey((string) $item['key']);
}
$validator = customApiValidator($item, [
'key' => 'string|required',
'key' => ValidationPatterns::environmentVariableKeyRules(),
'value' => 'string|nullable',
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
@@ -3473,8 +3481,12 @@ class DatabasesController extends Controller
$this->authorize('manageEnvironment', $database);
if ($request->has('key')) {
$request->merge(['key' => ValidationPatterns::normalizeEnvironmentVariableKey((string) $request->key)]);
}
$validator = customApiValidator($request->all(), [
'key' => 'string|required',
'key' => ValidationPatterns::environmentVariableKeyRules(),
'value' => 'string|nullable',
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
@@ -3759,10 +3771,11 @@ class DatabasesController extends Controller
'host_path' => ['string', 'nullable', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN],
'content' => 'string|nullable',
'is_directory' => 'boolean',
'is_host_file' => 'boolean',
'fs_path' => 'string',
]);
$allAllowedFields = ['type', 'name', 'mount_path', 'host_path', 'content', 'is_directory', 'fs_path'];
$allAllowedFields = ['type', 'name', 'mount_path', 'host_path', 'content', 'is_directory', 'is_host_file', 'fs_path'];
$extraFields = array_diff(array_keys($request->all()), $allAllowedFields);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
@@ -3786,7 +3799,7 @@ class DatabasesController extends Controller
], 422);
}
$typeSpecificInvalidFields = array_intersect(['content', 'is_directory', 'fs_path'], array_keys($request->all()));
$typeSpecificInvalidFields = array_intersect(['content', 'is_directory', 'is_host_file', 'fs_path'], array_keys($request->all()));
if (! empty($typeSpecificInvalidFields)) {
return response()->json([
'message' => 'Validation failed.',
@@ -3817,6 +3830,14 @@ class DatabasesController extends Controller
}
$isDirectory = $request->boolean('is_directory', false);
$isHostFile = $request->boolean('is_host_file', false);
if ($isDirectory && $isHostFile) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['is_host_file' => 'Host file mounts cannot also be directory mounts.'],
], 422);
}
if ($isDirectory) {
if (! $request->fs_path) {
@@ -3839,12 +3860,50 @@ class DatabasesController extends Controller
'resource_id' => $database->id,
'resource_type' => get_class($database),
]);
} elseif ($isHostFile) {
if (! $request->fs_path) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['fs_path' => 'The fs_path field is required for host file mounts.'],
], 422);
}
if ($request->filled('content')) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['content' => 'Content is not valid for host file mounts.'],
], 422);
}
try {
$fsPath = validateHostFileMountPath($request->fs_path, 'host file source path');
$mountPath = validateFileMountPath($request->mount_path, 'host file destination path');
} catch (\Throwable $e) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['mount_path' => $e->getMessage()],
], 422);
}
$storage = LocalFileVolume::create([
'fs_path' => $fsPath,
'mount_path' => $mountPath,
'content' => null,
'is_directory' => false,
'is_host_file' => true,
'resource_id' => $database->id,
'resource_type' => get_class($database),
]);
} else {
$mountPath = str($request->mount_path)->trim()->start('/')->value();
validateShellSafePath($mountPath, 'file storage path');
$fsPath = database_configuration_dir().'/'.$database->uuid.$mountPath;
try {
$mountPath = validateFileMountPath($request->mount_path, 'file storage path');
$fsPath = confineFileMountPath(database_configuration_dir().'/'.$database->uuid, $mountPath, 'file storage path');
} catch (\Throwable $e) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['mount_path' => $e->getMessage()],
], 422);
}
$storage = LocalFileVolume::create([
'fs_path' => $fsPath,
@@ -0,0 +1,239 @@
<?php
namespace App\Http\Controllers\Api;
use App\Actions\Destination\RemoveStandaloneDockerNetwork;
use App\Http\Controllers\Controller;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Illuminate\Database\QueryException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class DestinationsController extends Controller
{
private function transform(StandaloneDocker|SwarmDocker $destination): array
{
return [
'uuid' => $destination->uuid,
'name' => $destination->name,
'network' => $destination->network,
'type' => $destination instanceof SwarmDocker ? 'swarm' : 'standalone',
'server_uuid' => $destination->server?->uuid,
'created_at' => $destination->created_at,
'updated_at' => $destination->updated_at,
];
}
/**
* Resolve the calling token's team id, or return an invalid-token response.
*/
private function teamIdOrAbort(): int|JsonResponse
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
return $teamId;
}
/**
* StandaloneDocker / SwarmDocker scoped to a team via their parent server.
* Uses whereHas instead of the model's ownedByCurrentTeamAPI() scope so the
* controller works on Coolify versions that pre-date that scope being added
* to the destination models (e.g. 4.0.0-beta.470).
*/
private function teamScopedDockers(int $teamId): array
{
return [
'standalone' => StandaloneDocker::with('server:id,uuid')->whereHas('server', fn ($query) => $query->whereTeamId($teamId))->get(),
'swarm' => SwarmDocker::with('server:id,uuid')->whereHas('server', fn ($query) => $query->whereTeamId($teamId))->get(),
];
}
private function findDestinationForTeam(int $teamId, string $uuid): StandaloneDocker|SwarmDocker
{
return StandaloneDocker::with('server:id,uuid,team_id,ip,user,port,private_key_id')->whereHas('server', fn ($query) => $query->whereTeamId($teamId))->whereUuid($uuid)->first()
?? SwarmDocker::with('server:id,uuid,team_id')->whereHas('server', fn ($query) => $query->whereTeamId($teamId))->whereUuid($uuid)->firstOrFail();
}
public function index(Request $request): JsonResponse
{
$teamId = $this->teamIdOrAbort();
if (! is_int($teamId)) {
return $teamId;
}
$sets = $this->teamScopedDockers($teamId);
return response()->json(
$sets['standalone']->concat($sets['swarm'])
->map(fn ($destination) => $this->transform($destination))
->values()
);
}
public function index_by_server(Request $request, string $server_uuid): JsonResponse
{
$teamId = $this->teamIdOrAbort();
if (! is_int($teamId)) {
return $teamId;
}
$server = Server::with(['standaloneDockers.server:id,uuid', 'swarmDockers.server:id,uuid'])
->whereTeamId($teamId)
->whereUuid($server_uuid)
->firstOrFail();
$list = $server->standaloneDockers->concat($server->swarmDockers);
return response()->json($list->map(fn ($destination) => $this->transform($destination))->values());
}
public function show(Request $request, string $uuid): JsonResponse
{
$teamId = $this->teamIdOrAbort();
if (! is_int($teamId)) {
return $teamId;
}
$destination = $this->findDestinationForTeam($teamId, $uuid);
return response()->json($this->transform($destination));
}
public function create(Request $request, string $server_uuid): JsonResponse
{
$teamId = $this->teamIdOrAbort();
if (! is_int($teamId)) {
return $teamId;
}
$return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) {
return $return;
}
$server = Server::whereTeamId($teamId)->whereUuid($server_uuid)->firstOrFail();
$allowed = ['name', 'network', 'type'];
$validator = customApiValidator($request->all(), [
'name' => 'nullable|string|max:255',
'network' => ['required', 'string', 'max:255', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/'],
'type' => 'nullable|in:standalone,swarm',
]);
$extra = array_diff(array_keys($request->all()), $allowed);
if ($validator->fails() || ! empty($extra)) {
$errors = $validator->errors();
if (! empty($extra)) {
foreach ($extra as $field) {
$errors->add($field, 'This field is not allowed.');
}
}
return response()->json(['message' => 'Validation failed.', 'errors' => $errors], 422);
}
$expectedType = $server->isSwarm() ? 'swarm' : 'standalone';
$type = $request->input('type', $expectedType);
if ($type !== $expectedType) {
return response()->json(['message' => "Destination type must be {$expectedType} for this server."], 422);
}
$name = $request->input('name') ?: ($server->name.'-'.$request->input('network'));
$class = $type === 'swarm' ? SwarmDocker::class : StandaloneDocker::class;
$this->authorize('create', $class);
$exists = $class::where('server_id', $server->id)->where('network', $request->input('network'))->exists();
if ($exists) {
return response()->json(['message' => 'A destination with this network already exists on the server.'], 409);
}
try {
$destination = $class::create([
'name' => $name,
'network' => $request->input('network'),
'server_id' => $server->id,
]);
} catch (QueryException $exception) {
if ($this->isUniqueConstraintViolation($exception)) {
return response()->json(['message' => 'A destination with this network already exists on the server.'], 409);
}
throw $exception;
}
auditLog('api.destination.created', [
'team_id' => $teamId,
'destination_uuid' => $destination->uuid,
'destination_name' => $destination->name,
'destination_type' => $type,
'server_uuid' => $server->uuid,
]);
return response()->json($this->transform($destination->load('server:id,uuid')), 201);
}
private function isUniqueConstraintViolation(QueryException $exception): bool
{
$sqlState = $exception->errorInfo[0] ?? null;
$driverCode = (string) ($exception->errorInfo[1] ?? $exception->getCode());
return in_array($sqlState, ['23000', '23505'], true)
|| in_array($driverCode, ['19', '1062', '2067'], true);
}
public function delete(Request $request, string $uuid): JsonResponse
{
$teamId = $this->teamIdOrAbort();
if (! is_int($teamId)) {
return $teamId;
}
$destination = $this->findDestinationForTeam($teamId, $uuid);
$this->authorize('delete', $destination);
// Guard against deleting destinations with attached resources. attachedTo()
// is recent on the destination models; fall back to a manual check for
// older Coolify versions (e.g. 4.0.0-beta.470).
if (method_exists($destination, 'attachedTo')) {
if ($destination->attachedTo()) {
return response()->json(['message' => 'Destination has attached resources, detach first.'], 409);
}
} else {
$hasAttached = $destination->applications()->exists()
|| $destination->postgresqls()->exists()
|| (method_exists($destination, 'mysqls') && $destination->mysqls()->exists())
|| (method_exists($destination, 'mariadbs') && $destination->mariadbs()->exists())
|| (method_exists($destination, 'mongodbs') && $destination->mongodbs()->exists())
|| (method_exists($destination, 'redis') && $destination->redis()->exists())
|| (method_exists($destination, 'keydbs') && $destination->keydbs()->exists())
|| (method_exists($destination, 'dragonflies') && $destination->dragonflies()->exists())
|| (method_exists($destination, 'clickhouses') && $destination->clickhouses()->exists())
|| (method_exists($destination, 'services') && $destination->services()->exists());
if ($hasAttached) {
return response()->json(['message' => 'Destination has attached resources, detach first.'], 409);
}
}
if ($destination instanceof StandaloneDocker) {
app(RemoveStandaloneDockerNetwork::class)->handle($destination);
}
$destinationUuid = $destination->uuid;
$destinationName = $destination->name;
$destinationType = $destination instanceof SwarmDocker ? 'swarm' : 'standalone';
$serverUuid = $destination->server?->uuid;
$destination->delete();
auditLog('api.destination.deleted', [
'team_id' => $teamId,
'destination_uuid' => $destinationUuid,
'destination_name' => $destinationName,
'destination_type' => $destinationType,
'server_uuid' => $serverUuid,
]);
return response()->json(['message' => 'Deleted.']);
}
}
+69 -10
View File
@@ -1312,8 +1312,12 @@ class ServicesController extends Controller
$this->authorize('manageEnvironment', $service);
if ($request->has('key')) {
$request->merge(['key' => ValidationPatterns::normalizeEnvironmentVariableKey((string) $request->key)]);
}
$validator = customApiValidator($request->all(), [
'key' => 'string|required',
'key' => ValidationPatterns::environmentVariableKeyRules(),
'value' => 'string|nullable',
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
@@ -1461,8 +1465,12 @@ class ServicesController extends Controller
$updatedEnvs = collect();
foreach ($bulk_data as $item) {
if (array_key_exists('key', $item)) {
$item['key'] = ValidationPatterns::normalizeEnvironmentVariableKey((string) $item['key']);
}
$validator = customApiValidator($item, [
'key' => 'string|required',
'key' => ValidationPatterns::environmentVariableKeyRules(),
'value' => 'string|nullable',
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
@@ -1580,8 +1588,12 @@ class ServicesController extends Controller
$this->authorize('manageEnvironment', $service);
if ($request->has('key')) {
$request->merge(['key' => ValidationPatterns::normalizeEnvironmentVariableKey((string) $request->key)]);
}
$validator = customApiValidator($request->all(), [
'key' => 'string|required',
'key' => ValidationPatterns::environmentVariableKeyRules(),
'value' => 'string|nullable',
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
@@ -2166,10 +2178,11 @@ class ServicesController extends Controller
'host_path' => ['string', 'nullable', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN],
'content' => 'string|nullable',
'is_directory' => 'boolean',
'is_host_file' => 'boolean',
'fs_path' => 'string',
]);
$allAllowedFields = ['type', 'resource_uuid', 'name', 'mount_path', 'host_path', 'content', 'is_directory', 'fs_path'];
$allAllowedFields = ['type', 'resource_uuid', 'name', 'mount_path', 'host_path', 'content', 'is_directory', 'is_host_file', 'fs_path'];
$extraFields = array_diff(array_keys($request->all()), $allAllowedFields);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
@@ -2201,7 +2214,7 @@ class ServicesController extends Controller
], 422);
}
$typeSpecificInvalidFields = array_intersect(['content', 'is_directory', 'fs_path'], array_keys($request->all()));
$typeSpecificInvalidFields = array_intersect(['content', 'is_directory', 'is_host_file', 'fs_path'], array_keys($request->all()));
if (! empty($typeSpecificInvalidFields)) {
return response()->json([
'message' => 'Validation failed.',
@@ -2232,6 +2245,14 @@ class ServicesController extends Controller
}
$isDirectory = $request->boolean('is_directory', false);
$isHostFile = $request->boolean('is_host_file', false);
if ($isDirectory && $isHostFile) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['is_host_file' => 'Host file mounts cannot also be directory mounts.'],
], 422);
}
if ($isDirectory) {
if (! $request->fs_path) {
@@ -2254,12 +2275,50 @@ class ServicesController extends Controller
'resource_id' => $subResource->id,
'resource_type' => get_class($subResource),
]);
} elseif ($isHostFile) {
if (! $request->fs_path) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['fs_path' => 'The fs_path field is required for host file mounts.'],
], 422);
}
if ($request->filled('content')) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['content' => 'Content is not valid for host file mounts.'],
], 422);
}
try {
$fsPath = validateHostFileMountPath($request->fs_path, 'host file source path');
$mountPath = validateFileMountPath($request->mount_path, 'host file destination path');
} catch (\Throwable $e) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['mount_path' => $e->getMessage()],
], 422);
}
$storage = LocalFileVolume::create([
'fs_path' => $fsPath,
'mount_path' => $mountPath,
'content' => null,
'is_directory' => false,
'is_host_file' => true,
'resource_id' => $subResource->id,
'resource_type' => get_class($subResource),
]);
} else {
$mountPath = str($request->mount_path)->trim()->start('/')->value();
validateShellSafePath($mountPath, 'file storage path');
$fsPath = service_configuration_dir().'/'.$service->uuid.$mountPath;
try {
$mountPath = validateFileMountPath($request->mount_path, 'file storage path');
$fsPath = confineFileMountPath(service_configuration_dir().'/'.$service->uuid, $mountPath, 'file storage path');
} catch (\Throwable $e) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['mount_path' => $e->getMessage()],
], 422);
}
$storage = LocalFileVolume::create([
'fs_path' => $fsPath,
+4 -40
View File
@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Support\DatabaseBackupFileValidator;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
@@ -16,24 +17,7 @@ class UploadController extends BaseController
private const MAX_BYTES = 10 * 1024 * 1024 * 1024; // 10 GiB
private const ALLOWED_EXTENSIONS = [
'sql',
'sql.gz',
'gz',
'zip',
'tar',
'tar.gz',
'tgz',
'dump',
'bak',
'bson',
'bson.gz',
'archive',
'archive.gz',
'bz2',
'xz',
'dmp',
];
private const ALLOWED_EXTENSIONS = DatabaseBackupFileValidator::ALLOWED_EXTENSIONS;
public function upload(Request $request)
{
@@ -85,10 +69,7 @@ class UploadController extends BaseController
protected function saveFile(UploadedFile $file, string $resourceIdentifier)
{
$originalName = $file->getClientOriginalName();
$size = $file->getSize();
if (! self::hasAllowedExtension($originalName) || $size === false || $size > self::MAX_BYTES) {
if (! DatabaseBackupFileValidator::isUploadAllowed($file, self::MAX_BYTES)) {
@unlink($file->getPathname());
return response()->json([
@@ -108,24 +89,7 @@ class UploadController extends BaseController
private static function hasAllowedExtension(string $name): bool
{
$lower = strtolower($name);
$suffixes = array_map(fn ($ext) => '.'.$ext, self::ALLOWED_EXTENSIONS);
usort($suffixes, fn ($a, $b) => strlen($b) <=> strlen($a));
foreach ($suffixes as $suffix) {
if (! str_ends_with($lower, $suffix)) {
continue;
}
$stem = substr($lower, 0, -strlen($suffix));
if ($stem !== '' && ! str_ends_with($stem, '.')) {
return true;
}
return false;
}
return false;
return DatabaseBackupFileValidator::hasAllowedExtension($name);
}
private static function formatMaxSize(): string
+10
View File
@@ -261,6 +261,16 @@ class Github extends Controller
return response('Nothing to do. No GitHub App found.');
}
$webhook_secret = data_get($github_app, 'webhook_secret');
if (empty($webhook_secret)) {
auditLogWebhookFailure('github', 'webhook_secret_missing', [
'mode' => 'app',
'github_app_id' => $github_app->id,
'github_app_name' => $github_app->name,
'installation_target_id' => $x_github_hook_installation_target_id,
]);
return response('Invalid signature.');
}
$hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret);
if (config('app.env') !== 'local') {
if (! hash_equals($x_hub_signature_256, $hmac)) {