fix(security): stop leaking locked shared variables and proxy secrets

- References to locked (shown once) shared variables keep their reference
  text in the UI, copy action, and API instead of the locked value.
- Remove the saved proxy configuration and validation logs from nested
  servers (for example destination.server) in API responses unless the
  token can read sensitive data.
- The services list API now applies removeSensitiveData to every service.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-10-02 16:01:00 +02:00
co-authored by Claude Opus 5.5
parent a5ec854e35
commit aa2592b17c
7 changed files with 423 additions and 54 deletions
+67 -4
View File
@@ -7,6 +7,7 @@ use App\Enums\StaticImageTypes;
use App\Models\Application;
use App\Models\ApplicationSetting;
use App\Models\Environment;
use App\Models\EnvironmentVariable;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use App\Rules\ManualWebhookSecret;
@@ -16,6 +17,7 @@ use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection as SupportCollection;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
@@ -42,7 +44,7 @@ function serializeApiResponse($data)
{
if ($data instanceof Collection) {
return $data->map(function ($d) {
$d = collect($d)->sortKeys();
$d = collect(apiEnvironmentVariableDisplayValue($d))->sortKeys();
$created_at = data_get($d, 'created_at');
$updated_at = data_get($d, 'updated_at');
if ($created_at) {
@@ -67,10 +69,10 @@ function serializeApiResponse($data)
$d = $d->prepend($d['id'], 'id');
}
return $d;
return removeServerProxySecretsFromApiPayload($d);
});
} else {
$d = collect($data)->sortKeys();
$d = collect(apiEnvironmentVariableDisplayValue($data))->sortKeys();
$created_at = data_get($d, 'created_at');
$updated_at = data_get($d, 'updated_at');
if ($created_at) {
@@ -95,10 +97,71 @@ function serializeApiResponse($data)
$d = $d->prepend($d['id'], 'id');
}
return $d;
return removeServerProxySecretsFromApiPayload($d);
}
}
/**
* Serialize an environment variable with its display value, so a reference to a
* locked shared variable keeps its reference text instead of the locked value.
*/
function apiEnvironmentVariableDisplayValue(mixed $data): mixed
{
if (! $data instanceof EnvironmentVariable || in_array('real_value', $data->getHidden(), true)) {
return $data;
}
return [...$data->toArray(), 'real_value' => $data->displayRealValue()];
}
/**
* Remove the saved proxy configuration and validation logs from every
* serialized server in an API payload, including nested ones such as
* `destination.server`, unless the token can read sensitive data.
* The saved proxy configuration often contains DNS provider API tokens.
*
* @template T
*
* @param T $payload
* @return T
*/
function removeServerProxySecretsFromApiPayload(mixed $payload): mixed
{
if (request()->attributes->get('can_read_sensitive', false) === true) {
return $payload;
}
return removeServerProxySecrets($payload);
}
/**
* @template T
*
* @param T $value
* @return T
*/
function removeServerProxySecrets(mixed $value): mixed
{
if (! is_array($value) && ! $value instanceof SupportCollection) {
return $value;
}
$proxy = $value['proxy'] ?? null;
if (is_array($proxy)) {
unset($proxy['last_saved_proxy_configuration']);
$value['proxy'] = $proxy;
unset($value['validation_logs']);
}
foreach ($value as $key => $item) {
if (is_array($item) || $item instanceof SupportCollection) {
$value[$key] = removeServerProxySecrets($item);
}
}
return $value;
}
/**
* Re-expose a model's `$hidden` sensitive fields when the current API request
* carries the `read:sensitive` or `root` token ability (set by the