0% прочитано

PHP __debugInfo(): як безпечно налагоджувати об'єкти із секретними даними

Під час розробки AVN Center у модулі Product Registry з'явився об'єкт, який містить хеш секрету. Це значення потрібне програмі, але не повинно випадково потрапляти у виведення під час налагодження. Показую, як __debugInfo() дозволяє явно визначити безпечне представлення об'єкта і водночас залишити достатньо даних для пошуку проблем.

28 вересня 2026 р. 9 хв читанняPHP

Під час розробки ми постійно дивимося на стан об’єктів. Десь це dump(), десь dd(), var_dump(), налагоджувач або тимчасова діагностика під час тестування.

Зі звичайними даними це майже непомітно. Але ситуація змінюється, коли об’єкт містить пароль, ключ, токен або хеш секрету.

Під час розробки модуля Product Registry в AVN Center я зіткнулася саме з таким випадком. ProductRegistrationCredentialAuthenticationRecord містить і дані реєстраційного облікового запису, і CredentialSecretHash.

Сам хеш потрібний програмі. Але під час налагодження мені не потрібно бачити його значення. Більше того, краще зробити так, щоб воно взагалі не потрапляло у звичайне представлення об’єкта для діагностики.

Закритий property не означає, що його значення автоматично безпечне для будь-якого способу діагностики. Межу потрібно визначати окремо.

Реальний випадок: об’єкт містить секрет, але не повинен його показувати

PHP - Real Product Registry authentication record
final readonly class ProductRegistrationCredentialAuthenticationRecord{    public function __construct(        private ProductRegistrationCredential $credential,        private CredentialSecretHash $secretHash,    ) {}     public function credential(): ProductRegistrationCredential    {        return $this->credential;    }     public function secretHash(): CredentialSecretHash    {        return $this->secretHash;    }

У цього класу немає штучного прикладу з вигаданим password. Це справжній об’єкт, який використовується для роботи з реєстраційними даними продукту.

Він зберігає два об’єкти: ProductRegistrationCredential і CredentialSecretHash. Обидва потрібні подальшій логіці, тому просто прибрати хеш із класу не можна.

Але вимоги до роботи програми і вимоги до налагодження різні. Для роботи потрібен сам хеш. Для діагностики достатньо знати, що він є.

private і readonly вирішують інші задачі

У класі обидва properties позначені private, а сам клас є readonly. Це добре, але це не заміна контролю діагностичного представлення.

private визначає доступ до property через звичайний код. readonly захищає стан від повторної зміни після ініціалізації.

Жодне з цих правил не відповідає на інше питання: які дані про цей об’єкт безпечно показати людині під час налагодження?

Інкапсуляція стану і безпечне представлення для діагностики - це різні відповідальності.

Що дає __debugInfo()

PHP - Explicit diagnostic representation
public function __debugInfo(): array{    return [        'credential_id' =>            $this->credential                ->id()                ->toString(),         'registration_id' =>            $this->credential                ->registrationId()                ->toString(),         'environment' =>            $this->credential                ->environment()

__debugInfo() дозволяє класу явно визначити, яке представлення потрібно використовувати під час стандартного перегляду об’єкта для налагодження.

Для мене тут важлива не сама можливість повернути масив. Важливіше те, що клас перестає покладатися на випадкове представлення своїх внутрішніх полів.

Я сама визначаю, які значення корисні для пошуку проблеми, а які повинні залишитися прихованими.

Приховати все теж було б незручно

Можна було б зробити __debugInfo() майже порожнім. Це зменшило б ризик витоку, але сильно погіршило б діагностику.

Коли я розбираю проблему з автентифікацією продукту, мені корисно бачити:

  • ідентифікатор облікового запису;
  • ідентифікатор реєстрації;
  • середовище;
  • профіль автентифікації;
  • поточний стан.

Ці дані дають достатньо контексту, щоб зрозуміти, з яким записом я працюю, але не розкривають сам секрет.

Чому я залишила secret_hash, але зі значенням [REDACTED]

PHP - Show presence without showing the secret
'secret_hash' => '[REDACTED]',

Я могла просто не додавати secret_hash до результату. Але позначка [REDACTED] дає ще одну корисну інформацію: поле навмисно приховане.

Це відрізняється від ситуації, коли значення взагалі відсутнє, не завантажене або випадково загубилося.

Під час налагодження я бачу, що об’єкт справді містить секретну складову, але її значення свідомо не показується.

Наявність секрету може бути корисною для діагностики. Значення секрету - майже ніколи.

Безпечніше явно перелічити дозволені поля

PHP - Do not expose internal state automatically
public function __debugInfo(): array{    return get_object_vars(        $this    );}

Я не хочу будувати діагностичне представлення за принципом "покажи все, крім кількох заборонених полів".

Для об’єкта із секретними даними надійніше працює протилежний підхід: явно перелічити тільки те, що дозволено показувати.

Тоді новий property не почне автоматично з’являтися в діагностиці лише тому, що хтось додав його до класу через кілька місяців.

Що я не хочу повертати з __debugInfo()

PHP - Returning internal sensitive objects
public function __debugInfo(): array{    return [        'credential' =>            $this->credential,         'secret_hash' =>            $this->secretHash,    ];}

Такий варіант майже не дає контролю.

Я повертаю внутрішні об’єкти цілком і фактично перекладаю відповідальність далі: тепер уже їхнє представлення повинно бути безпечним.

Для CredentialSecretHash це особливо небажано. Якщо мета поточного класу - гарантувати безпечний перегляд, краще взагалі не передавати секретний об’єкт у результат.

Наявність secretHash() не суперечить приховуванню

PHP - Application access is separate from diagnostic output
public function secretHash(): CredentialSecretHash{    return $this->secretHash;}

На перший погляд може виникнути питання: якщо клас має secretHash(), навіщо приховувати значення в __debugInfo()?

Бо це різні способи доступу з різною метою.

secretHash() існує для коду, якому цей value object справді потрібний. __debugInfo() існує для людини, яка оглядає стан об’єкта під час налагодження.

Доступ для програмної логіки не означає автоматичний дозвіл показувати ті самі дані у діагностичному виведенні.

__debugInfo() не є універсальним захистом секретів

Це дуже важлива межа. __debugInfo() контролює діагностичне представлення об’єкта в тих механізмах PHP, які використовують цей magic method.

Він не означає, що секрет автоматично захищений у всіх інших місцях.

Окремо потрібно контролювати:

  • перетворення об’єкта на масив;
  • JSON;
  • серіалізацію;
  • журнали застосунку;
  • контекст винятків;
  • дані для зовнішніх API;
  • метрики та телеметрію.

Безпечний __debugInfo() закриває одну конкретну межу. Він не повинен створювати хибне відчуття, що всі інші шляхи вже захищені.

__debugInfo() робить налагодження безпечнішим, але не робить секрет безпечним автоматично у всій системі.

Я сприймаю __debugInfo() як окремий діагностичний контракт

Потрібно для роботи класу

Потрібно для налагодження

ProductRegistrationCredential

credential_id

CredentialSecretHash

[REDACTED]

Повний об’єкт реєстраційних даних

registration_id

Повний стан сутності

environment

Об’єкти та value objects

authentication_profile і status

Цей поділ допоміг мені сформулювати просте правило: діагностичне представлення не повинно бути копією внутрішнього стану об’єкта.

Його задача - дати людині достатньо інформації для пошуку проблеми з мінімально необхідним обсягом даних.

Чому це корисно саме в реальній розробці

Витік секрету під час налагодження часто не виглядає як окрема велика помилка.

Розробник просто хоче зрозуміти, чому не проходить автентифікація, робить dump($record), копіює результат у повідомлення колезі, додає його в опис задачі або тимчасово залишає у журналі.

Чим менше ручної дисципліни потрібно для безпечної діагностики, тим краще.

Якщо сам клас уже знає, що секрет не повинен відображатися, це правило працює кожного разу автоматично для відповідного способу перегляду.

Для діагностики варто показувати мінімально необхідні дані

Я не намагаюся заховати все. Але для кожного поля ставлю просте питання: чи допоможе воно знайти проблему?

Ідентифікатори допоможуть знайти запис. Середовище допоможе зрозуміти, де він використовується. Профіль автентифікації і стан пояснять поточний режим роботи.

Сам хеш секрету майже нічого не додає до цієї діагностики. Отже показувати його немає сенсу.

Мій короткий підхід для об’єктів із секретними даними

  • Спочатку визначаю, які дані справді потрібні для діагностики.
  • Не повертаю внутрішній стан об’єкта автоматично.
  • Секретні значення не включаю у результат.
  • Якщо присутність секрету важлива, використовую явну позначку на кшталт [REDACTED].
  • Не покладаюся лише на private або readonly.
  • Окремо перевіряю JSON, серіалізацію, журнали та інші шляхи виведення даних.
  • Якщо до класу додається нове чутливе поле, воно не повинно автоматично з’явитися в діагностиці.

Коли __debugInfo() справді виправданий

Я б не додавала __debugInfo() до кожного DTO або кожної сутності.

Він особливо корисний там, де внутрішній стан і безпечне діагностичне представлення суттєво відрізняються.

Це можуть бути об’єкти з паролями, токенами, секретними ключами, підписами, хешами, приватними налаштуваннями підключення або іншими значеннями, які не повинні випадково копіюватися в діагностичне виведення.

Якщо ж об’єкт містить лише звичайні відкриті дані, окремий метод може не дати практичної користі.

Висновок

У випадку ProductRegistrationCredentialAuthenticationRecord мені не потрібно було змінювати саму модель даних або прибирати з неї CredentialSecretHash.

Потрібно було лише чітко розділити два поняття: що об’єкт повинен містити для роботи і що людина повинна бачити під час налагодження.

__debugInfo() дає для цього дуже просту точку контролю. Я залишаю ідентифікатори, середовище, профіль автентифікації та стан, а секретне значення замінюю на [REDACTED].

Це невеликий метод, але він прибирає один непотрібний ризик із повсякденної роботи.

Для мене __debugInfo() - це не спосіб зробити секрет безпечним. Це спосіб зробити саме налагодження безпечнішим.