NovinkaZverejnili sme 50+ Laravel balíkov ako open source
Custom AI apps, agents and automation — Roundly ConsultingRoundly
Všetky balíky
Package Toolkit for Laravel

Tajné hodnoty v konfigurácii a fasádach

API kľúče, podpisové tajomstvá, heslá a sady kľúčov potrebujú rovnako prísnu kontrolu ako každé iné nastavenie — nesprávne nastavená tajná hodnota sa však nikdy nesmie dostať do chybovej správy, stack trace ani do nástroja na sledovanie chýb. Toolkit pokrýva obe miesta, cez ktoré tajná hodnota balíkom prechádza: čítače konfigurácie aj fasádu.

Čítače tajných hodnôt

secret(), requireSecret() a secretList() čítajú ako string(), requireString() a list(), s dvoma rozdielmi. Chyby nikdy neukážu hodnotu: nesprávne nastavenú tajnú hodnotu opíšu len jej typom, napríklad Configuration value [comments.akismet_key] must be a non-empty string, [int] given., a položka, ktorú $each odmietne, znie must contain only valid items, [string] given. Hodnota nie je v správe ani v trace, nech ju nesie ktorákoľvek trieda výnimky (Config::for() / Config::using()). A predvolená hodnota z balíka neexistuje, pretože pre tajnú hodnotu žiadna rozumná nie je: secret() pri nenastavenom kľúči vráti null a secretList() vráti [], takže o tom, čo znamená „nenakonfigurované“, rozhodne volajúci; requireSecret() vyhodí is required but missing. Zadaný reťazec sa vráti tak, ako je, nikdy neorezaný:

use RoundlyConsulting\PackageToolkit\Support\Config;

// Required: not set throws "is required but missing"
$apiKey = Config::requireSecret('comments.akismet_key');

// Optional: not set → null, and the caller decides what "not configured" means
$apiKey = Config::secret('comments.akismet_key');

// config('comments.akismet_key') === 12345 — misconfigured, and the message names the type only:
// Configuration value [comments.akismet_key] must be a non-empty string, [int] given.
Hodnota v konfiguráciisecret()requireSecret()secretList()
Reťazec, napr. ' key '' key ' — tak, ako je, nikdy neorezaný' key '['key'] — položky orezané
'a, b''a, b''a, b'['a', 'b']
['a', 'b']Vyhodí [array] given.Vyhodí [array] given.['a', 'b']
Nenastavená: chýbajúca, null, '' alebo len medzerynullVyhodí is required but missing.[]
','','','[]
[] alebo [' ', '']Vyhodí [array] given.Vyhodí [array] given.[]
Int, float, bool, objektVyhodí [int] / [float] / [bool] / [ClassName] given.RovnakoVyhodí must be a list of strings …, [int] given.
Pole s položkou, ktorá nie je reťazcomVyhodí [array] given.RovnakoVyhodí must contain only string items, [int] given.

Sady kľúčov

secretList() spracúva hodnotu ako list(): publikované pole alebo zoznam z env oddelený čiarkami, položky orezané, prázdne vypustené, kľúče zahodené. Tajná hodnota s čiarkou preto nemôže byť v zozname z env — publikujte namiesto toho konfiguračné pole. $each dostane každú položku, preto jej parameter označte #[SensitiveParameter]: rámec vašej vlastnej closure toolkit skryť nedokáže a closure, ktorá vyhodí výnimku, by inak položku dostala do trace:

use RoundlyConsulting\PackageToolkit\Support\Config;
use SensitiveParameter;

$keys = Config::secretList('comments.signing_keys', static fn (#[SensitiveParameter] string $key): bool => strlen($key) >= 32);
$current = $keys[0] ?? throw new RuntimeException('Set COMMENTS_SIGNING_KEYS.');

Vlastné správy okolo tajnej hodnoty

Každá továrenská metóda InvalidConfigurationException opíše hodnotu zabalenú do SensitiveParameterValue z PHP typom toho, čo obaľuje, nikdy jej obsahom. Využite to, keď váš balík skladá vlastnú správu okolo tajnej hodnoty:

use RoundlyConsulting\PackageToolkit\Exceptions\InvalidConfigurationException;
use SensitiveParameterValue;

throw InvalidConfigurationException::notAString($key, new SensitiveParameterValue($value)); // "[int] given."

Hodnoty v rámcoch stack trace

Každá výnimka si zaznamená argumenty všetkých funkcií na zásobníku, pokiaľ ich v produkcii neodstráni zend.exception_ignore_args — a nástroje na sledovanie chýb, ktoré zbierajú argumenty rámcov, print_r($e) či vlastné loggery trace ich serializujú. Každý parameter, ktorý nesie konfiguračnú hodnotu, má preto #[SensitiveParameter]: $default každého čítača, pole odovzdané do Config::for() / ConfigValidator::forArray() aj hodnota v každej továrenskej metóde InvalidConfigurationException. Nesprávne nastavená hodnota sa ukáže v správe bežných čítačov, ktoré ju uvádzajú zámerne, a nikde inde. Čítače tajných hodnôt ju nepustia ani do správy.

Tajné hodnoty odovzdané cez fasádu

PHP nahradí argument s #[SensitiveParameter] objektom SensitiveParameterValue len v rámci funkcie, ktorá atribút deklaruje. Volanie cez bežnú fasádu Laravelu prejde najprv cez Facade::__callStatic($method, $args) a tento rámec nesie všetky argumenty v surovej podobe:

Crypto::constantTimeEquals($known, $userInput);
// #0 CryptoManager->constantTimeEquals(Object(SensitiveParameterValue), Object(SensitiveParameterValue))
// #1 Facade::__callStatic('constantTimeEquals', Array)   ← Array holds $known and $userInput raw

getTraceAsString() vypíše pole tohto rámca ako Array, takže tam sa tajná hodnota neukáže. Ukáže sa však všade, kde sa argumenty rámcov serializujú: v nástrojoch na sledovanie chýb, ktoré ich zbierajú, v print_r($e) / var_dump($e) či vo vlastných loggeroch trace. Formátovač logov a chybová stránka Laravelu ukazujú len typy argumentov a produkčné zend.exception_ignore_args = On argumenty rámcov úplne odstráni, takže k úniku dôjde len pri vypnutom nastavení. To isté volanie cez dependency injection bolo skryté už predtým.

Túto medzeru uzavrie trait RedactsSensitiveArguments: v rámci samotnej fasády skryje presne tie argumenty, ktoré metóda koreňového objektu fasády označuje #[SensitiveParameter], a neškodné argumenty nechá viditeľné. Pridajte ho do fasády — nič iné sa nemení:

use Illuminate\Support\Facades\Facade;
use RoundlyConsulting\PackageToolkit\Concerns\RedactsSensitiveArguments;

/**
 * @method static bool constantTimeEquals(string $known, string $userInput)
 *
 * @see CryptoManager
 */
final class Crypto extends Facade
{
    use RedactsSensitiveArguments;

    protected static function getFacadeAccessor(): string
    {
        return CryptoManager::class;
    }
}

Neudržiavate žiadny zoznam metód. Jediným zdrojom sú atribúty #[SensitiveParameter] koreňového objektu, takže nový tajný parameter sa skryje hneď, ako ho označíte.

Čo trait pokrýva

VolanieRámec fasády ukáže
Crypto::check($secret, $label)['check', [Object(SensitiveParameterValue), $label]]
Crypto::check(label: $label, secret: $secret)['label' => $label, 'secret' => Object(SensitiveParameterValue)]
Crypto::seal($label, ...$parts)Citlivý variadický parameter: každá zozbieraná časť je skrytá, pozičná aj pomenovaná.
Crypto::tag($secret, ...$labels)Neškodný variadický parameter vedľa tajnej hodnoty: tajná hodnota je skrytá, popisy zostanú viditeľné.
Crypto::check(secrett: $secret)Preklep v pomenovanom argumente: skrytý (volanie aj tak zlyhá s Unknown named parameter).
Metóda bez #[SensitiveParameter]Všetky argumenty, ako doteraz.
Koreňový objekt fasády sa nepodarí získať (kontajner vyhodí výnimku)Skryté — argumenty sa skryjú ešte predtým, než sa koreňový objekt získa.

Trait nahrádza Facade::__callStatic(). Citlivé položky vo vlastnom $args zabalí — backtrace ukazuje aktuálnu hodnotu parametra — a ďalej odovzdá nedotknutú kópiu, takže koreňový objekt dostane skutočné hodnoty.

Odkiaľ sa berú pozície

Trait reflexiou preskúma typ accessora: triedu alebo rozhranie, ktoré vracia getFacadeAccessor(). Vyhľadanie prebehne raz pre každú triedu fasády a metódu a výsledok sa uchová v cache na celý proces.

  • Rozhrania ako accessor — parametre označte na rozhraní. Fasáda vidí rozhranie, nie triedu, ktorá je naň naviazaná.
  • Fake a mocky — fake vymenený cez swap() / fake() alebo Mockery mock zo shouldReceive() dostane skutočné argumenty a zaznamená si ich pre svoje asercie. Rámec fasády zostane skrytý aj vtedy, keď prepísaná metóda vo fake atribút vynechá; o vlastný rámec sa stará fake.
  • Čo pokryté nie je — accessor, ktorý nie je class-string (alias v kontajneri, napríklad 'crypto'), a metóda, ktorú typ accessora nedeklaruje (makro, ktoré koreňový objekt preposiela cez __call), sa odovzdajú bez skrytia, presne ako pri bežnej fasáde. Tajné hodnoty vnútri objektov odovzdaných ako argumenty — DTO či objekt s prihlasovacími údajmi — sa ukážu ako samotný objekt; skryť ich obsah je úlohou objektu.

Čo zostáva nezmenené

  • Návratové hodnoty a výnimky pochádzajú z koreňového objektu, bez zmeny.
  • Prevod skalárnych typov sa zhoduje s Laravelom. Facade::__callStatic() je v súbore bez strict_types, takže Crypto::base64Encode(123) zo striktného súboru prevedie 123 na '123', kým to isté volanie cez dependency injection vyhodí TypeError. Striktnosť určuje súbor, v ktorom je volanie napísané, preto súbor traitu zámerne strict_types nemá a tie isté volania zostanú prijaté.
  • „A facade root has not been set.“ sa vyhodí ako doteraz, keď nie je nastavená aplikácia.
  • Réžia: približne o 170 ns viac na volanie metódy s tajnou hodnotou a približne o 30 ns pri metóde bez nej, oproti približne 250 ns pri volaní bežnej fasády (PHP 8.4 CLI, zapnutý opcache).

Testovanie skrytia

Rámec fasády čítajte so zapnutými argumentmi rámcov. zend.exception_ignore_args je v produkčnom ini zapnuté — aj na CI runneroch, ktoré ho používajú — a rámec bez argumentov nič nedokazuje:

it('hides the secret in the facade frame', function (): void {
    $previous = ini_set('zend.exception_ignore_args', '0');

    try {
        Crypto::base64UrlDecode('*not-base64*');
    } catch (InvalidEncodingException $e) {
        $frame = collect($e->getTrace())->firstWhere('function', '__callStatic');
    } finally {
        ini_set('zend.exception_ignore_args', (string) $previous);
    }

    expect($frame['args'][0])->toBe('base64UrlDecode')
        ->and($frame['args'][1][0])->toBeInstanceOf(SensitiveParameterValue::class);
});

Prejavte lásku k open source

Tento balík je zadarmo pod licenciou MIT. Ak vám šetrí čas, jednorazový príspevok alebo členstvo na Patreone nám pomôže ho ďalej udržiavať, testovať a dokumentovať.

Ďalšie spôsoby podpory vrátane kryptomien

Odoslaním daru súhlasíte s našimi podmienkami prijímania darov.

Chcete to zabudovať do svojho produktu?

Naše balíky integrujeme do zákazkových Laravel a AI riešení. Napíšte nám, na čom pracujete, a ozveme sa do 48 hodín.