【AIセキュリティ入門 #1】AIアプリの安全なログ管理 — 障害調査と個人情報保護を両立する(Laravel編) #PHP - Qiita
@Takahiro_Nago(Nago Takahiro)

【Introduction to AI Security #1】Secure Log Management for AI Applications — Balancing Incident Investigation and Personal Information Protection (Laravel Edition)

  • Laravel
  • PHP
  • Security
  • Logs
  • AI
0
Posted at 2026-11-10

Introduction

In a web application that incorporates AI, text entered by the user (consultation content, questions, chat messages, etc.) is sent directly to the API, and the results are returned.
These “inputs” and “outputs” are the data most likely to contain personal information within the application.

However, if you increase logging for incident investigation, these inputs and outputs become more likely to be mixed in with the log entries. Conversely, if you reduce logging too much, it becomes impossible to determine what happened.

In this article, we explain how to keep the consultation content itself out of the logs while retaining only the information needed for troubleshooting, using Laravel’s actual configuration and code as the basis.

This article compiles notes from the AI security studies I have been doing at a pace of 15 minutes every day. Since this is my first post on Qiita, I have written it so that it can be read without any prerequisite knowledge.

The Application Used as an Example

I will use “ZEN,” an AI consultation web application that I develop as a personal project. I will include only the necessary background so that it can be understood even if you are not familiar with it.

  • When a user enters their concerns in a form, the AI (OpenAI API) returns a response
  • Implemented in Laravel 13 / PHP 8.3
  • The consultation text is referred to as “concern,” and the AI response as “answer”

In other words, it is a perfectly ordinary web application that passes the user’s free-form input to AI. The same approach applies as-is to any application with the same structure.

Today’s Learning Goals

  1. Be able to explain the difference between APP_DEBUG and LOG_LEVEL
  2. Understand how Laravel automatically records exceptions in the logs
  3. Be able to record the information needed for incident investigation without saving the consultation content

1. APP_DEBUG and LOG_LEVEL Are Different Things

First, let us consider separately the three settings that are often confused. Laravel’s .env file contains lines such as the following.

APP_DEBUG=true LOG_CHANNEL=stack LOG_LEVEL=debug
Settings Purpose Basic policy for the production environment
APP_DEBUG Whether to display error details on the user's screen Always false
LOG_LEVEL Which logs of a specified severity level or higher to save Set according to operational requirements
LOG_CHANNEL Log output destination (file, syslog, Slack, etc.) Set according to the operating environment

APP_DEBUG concerns information shown to users, while LOG_LEVEL and LOG_CHANNEL concern information retained on the server side. Changing one does not affect the other.

Order of log levels

Laravel's log levels follow PSR-3 and are listed below in descending order of severity.

Level Meaning
emergency System is unavailable
alert Immediate action is required
critical Serious failure
error An error has occurred
warning Warning or a condition that should be noted
notice An unusual but important condition
info Routine operational information
debug Information for development and detailed investigation

LOG_LEVEL records logs at the specified level and those with a higher severity.
For example, when LOG_LEVEL=warning is set, warning / error / critical … are recorded, while info and debug are not.

Changing it to LOG_LEVEL=warning alone does not automatically mask personal information.
If a log at the error level contains the consultation content, it will remain as is. Log level controls the "amount," not the safety of the "content."

Also, Laravel's official documentation explicitly states that APP_DEBUG must always be set to false in production. If it remains true, environment variables and stack traces may be displayed on the error screen, potentially leaking sensitive information to end users.

2. Exceptions are logged even if you "do nothing"

This is the most important point this time.

Re-throwing with throw $exception causes the global handler to log it

ZEN's AI consultation processing includes the following catch. This is the processing used to return consumed credits when an AI call fails.

} catch (Throwable $exception) { // Cleanup, such as returning credits throw $exception; }

This code itself does not write a log.
However, when an exception is re-thrown with throw $exception, processing is passed to Laravel's global exception handler,and the exception is reported and logged according to the standard configuration.

In other words, the claim that "because I did not write a log myself, nothing will remain in the log" is incorrect.

report() works the same way

Code that calls report() as follows also logs the exception.

catch (ApiErrorException $exception) { report($exception); }

report() is the function to use when you want to "swallow the exception while still leaving it in the log."
bootstrap/app.php でカスタムの報告処理(Slack通知など)を追加しても、例外は引き続き標準ログに記録されます。特定の例外を報告対象から外したい場合は、dontReport() メソッドや ShouldntReport インターフェースを使います。

各対策にはそれぞれ異なる役割があります

ZENでは、相談文をログに残さないための対策を複数重ねています。似たように見えますが、それぞれ守っている場所が違います。

#[\SensitiveParameter] (PHP 8.2+) $exceptions->dontFlash(['concern'])
対策 守っている場所
zend.exception_ignore_args=1 (php.ini) 例外のスタックトレースに 関数の引数 を含めない
特定の引数だけをトレース上で SensitiveParameterValue に置き換える
バリデーションエラー時に相談文を セッションにフラッシュ しない
例外報告の制御(dontReport() など) どの例外を どこに 記録するかを管理する

実際のコードでは、相談文を受け取る関数の引数に属性を付けています。

private function consultationTopic(#[\SensitiveParameter] string $concern): string

例外のメッセージ自体に相談文が含まれていれば、引数を隠す設定だけでは防げません。
たとえば new RuntimeException("Failed: {$concern}") のようなコードを書いてしまうと、上の対策はすべて無効になります。例外メッセージには、ユーザー入力を直接埋め込まないようにします。

3. ログには「相談内容」ではなく「調査に必要な情報」を残す

Unsafe example

Log::error('AI consultation failed.', [ 'concern' => $concern, 'answer' => $answer, 'api_key' => $apiKey, ]);

The consultation content, AI response, and API key are written to the log file as-is.

Because log files are copied to backups, log aggregation services, monitoring tools, and other destinations, once written, they are almost impossible to retrieve.

Safe example

Log::warning('AI consultation failed.', [ 'request_id' => $requestId, 'operation' => 'openai_chat_completion', 'exception_class' => $exception::class, ]);
Item Purpose
request_id Identifies which request the error occurred in
operation Identifies which operation failed
exception_classIdentify the type of exception

With these three, you can track “when” the failure occurred, “which process” it occurred in, and “what type of error” it was.
There is no need to investigate what the user was asking about.

Automatically attach a request ID to all logs

ZEN provides middleware that issues a request ID and registers it in Laravel's Context. Values placed in the Context are automatically attached to every log entry during that request.

class AssignRequestId { public const HEADER = 'X-Request-Id'; public function handle(Request $request, Closure $next): Response { $requestId = (string) Str::uuid(); Context::add('request_id', $requestId); $response = $next($request);$response->headers->set(self::HEADER, $requestId); return $response; } }

The same ID is also returned in the response headers, so when a user reports that an error has occurred, you can cross-reference the logs using only the ID displayed on the screen. There is no need to ask the user to describe the issue.

Logs themselves must also be protected

Laravel's standard exception handler automatically adds the logged-in user's ID to the exception log context when there is a logged-in user. This is useful for investigations, but it also means thatthe logs contain information that can identify an individual.

Log files also require access restrictions and management of their retention periods.

The OWASP Logging Cheat Sheet also categorizes these as follows:

  • What should be logged: Successful and failed authentication, access control failures, application errors, input validation failures, etc.
  • What should not be logged: Session IDs, access tokens, passwords, personal information, cryptographic keys, secret keys, etc.

4. Practical exercise: Check environment-specific settings in your own app

No code changes are needed. It is sufficient simply to understand how the settings differ between local and production environments.

Exercise A: Check the local settings

Run this from the project root.

PowerShell:

Select-String -Path .\.env -Pattern '^(APP_ENV|APP_DEBUG|LOG_CHANNEL|LOG_STACK|LOG_LEVEL)='

macOS / Linux:

grep -E '^(APP_ENV|APP_DEBUG|LOG_CHANNEL|LOG_STACK|LOG_LEVEL)=' .env

If LOG_STACK is not output, the default value of single in config/logging.php will be used. In other words, the output destination is storage/logs/laravel.log.

Task B: Check the same settings on the production server

SSH into the production server and run the same grep command in the project directory.

This command displays only the five specified items. Do not cat the entire .env file or paste it into chat or task management tools. It contains API keys and DB passwords.

Pay particular attention to the following two points:

  • Whether APP_ENV=production
  • Whether APP_DEBUG=false

Even in production, leaving LOG_LEVEL=debug does not itself immediately imply a data leak. However, it is worth considering whether it can reduce unnecessary detailed logs or, conversely, result in losing information necessary for operations.

Task C: Classify the information to be logged

Try classifying the information handled by your app as shown in the table below.

Information Logging policy
Request ID Record
AI model name Record as needed
User's input body Do not record
AI response body Do not record
API key Never record
Exception class name Record as needed
Exception message Handle carefully after checking for the inclusion of sensitive information
Processing time Record as needed

The key point is that "Exception message" is marked as "Handle carefully."Exceptions thrown by external libraries may include part of the request body.

5. Review Questions

Question 1: What is the primary meaning of LOG_LEVEL=debug?

  1. Display detailed errors to users
  2. Record logs of all severity levels, including debug
  3. Automatically remove personally identifiable information from logs
Correct answer

2

LOG_LEVEL determines the minimum severity of logs to record. Displaying information to users is handled by APP_DEBUG, which is a separate setting.

Question 2: Which combination is appropriate for troubleshooting issues without retaining the user's inquiry in the logs?

  1. Record the inquiry body, AI response, and API key
  2. Unconditionally record only the exception messages
  3. Record the request ID, process name, exception class name, and similar information
Correct answer

3

This retains identifying information for tracing the processing rather than confidential data. Because input text may sometimes be mixed into exception messages through external libraries, recording exception messages "unconditionally" is dangerous.

Summary

  • APP_DEBUG is the setting for "what to show users," while LOG_LEVEL is the setting for "what to retain on the server." They are separate settings.
  • throw $exception and report() are logged by the global handler even if you do not write logs yourself
  • exception_ignore_args, #[\SensitiveParameter], and dontFlash() each protect a different area. None of them can prevent user input from being embedded in exception messages
Logs should record "what happened." There is no need to record "what the user inquired about"

References (Primary Sources)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
Sign upLogin