fix(env): skip escaping for valid JSON in environment variables (#6160)

Prevent double-escaping of COMPOSER_AUTH and other JSON environment variables
by detecting valid JSON objects/arrays in realValue() and skipping quote
escaping entirely. This fixes broken JSON values passed to runtime services
while maintaining proper escaping for non-JSON values.

- Add JSON detection before escaping logic in EnvironmentVariable::realValue()
- JSON objects/arrays pass through unmodified, avoiding quote corruption
- Add comprehensive test coverage for JSON vs non-JSON escaping behavior
This commit is contained in:
Andras Bacsai 2026-01-28 10:59:00 +01:00
parent 351b250739
commit c0dadc003d
4 changed files with 133 additions and 20 deletions

View file

@ -123,6 +123,12 @@ public function realValue(): Attribute
} }
$real_value = $this->get_real_environment_variables($this->value, $resource); $real_value = $this->get_real_environment_variables($this->value, $resource);
// Skip escaping for valid JSON objects/arrays to prevent quote corruption (see #6160)
if (json_validate($real_value) && (str_starts_with($real_value, '{') || str_starts_with($real_value, '['))) {
return $real_value;
}
if ($this->is_literal || $this->is_multiline) { if ($this->is_literal || $this->is_multiline) {
$real_value = '\''.$real_value.'\''; $real_value = '\''.$real_value.'\'';
} else { } else {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,107 @@
<?php
use App\Models\EnvironmentVariable;
/**
* Tests for GitHub Issue #6160: COMPOSER_AUTH environment variable escaping.
*
* PR #6146 moved escaping into the EnvironmentVariable::realValue() accessor,
* causing double-escaping for build-time vars and broken JSON for runtime vars.
*
* Fix: JSON objects/arrays detected in realValue() skip escaping entirely.
*/
const COMPOSER_AUTH_JSON = '{"http-basic":{"backpackforlaravel.com":{"username":"ourusername","password":"ourpassword"}}}';
// ---------------------------------------------------------------------------
// Test 1: realValue accessor returns raw JSON for non-literal env vars
// ---------------------------------------------------------------------------
it('realValue accessor returns raw JSON without escaping quotes', function () {
$env = Mockery::mock(EnvironmentVariable::class)->makePartial();
$env->shouldReceive('relationLoaded')->with('resourceable')->andReturn(true);
$env->shouldReceive('getAttribute')->with('resourceable')->andReturn(new stdClass);
$env->shouldReceive('getAttribute')->with('value')->andReturn(COMPOSER_AUTH_JSON);
$env->shouldReceive('getAttribute')->with('is_literal')->andReturn(false);
$env->shouldReceive('getAttribute')->with('is_multiline')->andReturn(false);
$realValue = $env->real_value;
// JSON should pass through without escaping
expect($realValue)->toBe(COMPOSER_AUTH_JSON);
expect($realValue)->not->toContain('\\"');
});
// ---------------------------------------------------------------------------
// Test 2: realValue for a literal JSON env also returns raw JSON
// (JSON check fires before the literal single-quote wrapping)
// ---------------------------------------------------------------------------
it('realValue accessor for literal JSON env returns raw value without wrapping', function () {
$env = Mockery::mock(EnvironmentVariable::class)->makePartial();
$env->shouldReceive('relationLoaded')->with('resourceable')->andReturn(true);
$env->shouldReceive('getAttribute')->with('resourceable')->andReturn(new stdClass);
$env->shouldReceive('getAttribute')->with('value')->andReturn(COMPOSER_AUTH_JSON);
$env->shouldReceive('getAttribute')->with('is_literal')->andReturn(true);
$env->shouldReceive('getAttribute')->with('is_multiline')->andReturn(false);
$realValue = $env->real_value;
// JSON check should fire first, returning raw JSON without single-quote wrapping
expect($realValue)->toBe(COMPOSER_AUTH_JSON);
expect($realValue)->not->toStartWith("'");
expect($realValue)->not->toEndWith("'");
});
// ---------------------------------------------------------------------------
// Test 3: Non-JSON values still get normal escaping (regression check)
// ---------------------------------------------------------------------------
it('realValue accessor still escapes non-JSON values with quotes', function () {
$env = Mockery::mock(EnvironmentVariable::class)->makePartial();
$env->shouldReceive('relationLoaded')->with('resourceable')->andReturn(true);
$env->shouldReceive('getAttribute')->with('resourceable')->andReturn(new stdClass);
$env->shouldReceive('getAttribute')->with('value')->andReturn('hello "world"');
$env->shouldReceive('getAttribute')->with('is_literal')->andReturn(false);
$env->shouldReceive('getAttribute')->with('is_multiline')->andReturn(false);
$realValue = $env->real_value;
// Non-JSON should still be escaped by escapeEnvVariables
expect($realValue)->toContain('\\"');
expect($realValue)->toBe('hello \\"world\\"');
});
// ---------------------------------------------------------------------------
// Test 4: JSON array values also skip escaping
// ---------------------------------------------------------------------------
it('realValue accessor returns raw JSON array without escaping', function () {
$jsonArray = '[{"host":"example.com","token":"abc123"}]';
$env = Mockery::mock(EnvironmentVariable::class)->makePartial();
$env->shouldReceive('relationLoaded')->with('resourceable')->andReturn(true);
$env->shouldReceive('getAttribute')->with('resourceable')->andReturn(new stdClass);
$env->shouldReceive('getAttribute')->with('value')->andReturn($jsonArray);
$env->shouldReceive('getAttribute')->with('is_literal')->andReturn(false);
$env->shouldReceive('getAttribute')->with('is_multiline')->andReturn(false);
$realValue = $env->real_value;
expect($realValue)->toBe($jsonArray);
expect($realValue)->not->toContain('\\"');
});
// ---------------------------------------------------------------------------
// Test 5: Buildtime escaping of raw JSON produces recoverable value
// ---------------------------------------------------------------------------
it('escapeBashDoubleQuoted on raw JSON produces value recoverable as valid JSON', function () {
$escaped = escapeBashDoubleQuoted(COMPOSER_AUTH_JSON);
// Should be double-quoted
expect($escaped)->toStartWith('"');
expect($escaped)->toEndWith('"');
// After bash unescaping (strip outer quotes, unescape \")
$inner = substr($escaped, 1, -1);
$inner = str_replace('\\"', '"', $inner);
$decoded = json_decode($inner, true);
expect($decoded)->not->toBeNull("Expected valid JSON after bash unescaping, got: {$inner}");
expect($decoded)->toHaveKey('http-basic');
});