Companion Guide: This guide belongs to the Laravel kits. Every Laravel kit ships these checks built in. Explore Kit #1 (Filament), Kit #2 (Livewire), and Kit #3 (MongoDB).
Track 05 Lesson 18

Model Output Validation in Laravel

Curated by Umar Farooq • 5 min read
Quick Answer & Core Takeaway

Model output validation treats all generated responses from an AI system as completely untrusted input. Large Language Models can hallucinate, be coerced into malicious outputs via indirect prompt injection, or generate syntax payloads designed to exploit downstream systems. Never pass LLM outputs directly into raw database queries, shell exec commands, eval functions, or unescaped Blade {!! $output !!} tags. Always validate schemas, type-cast fields, and escape data based on downstream context.

Last reviewed: 2026-10. Applies to: PHP 8.4+, Laravel 11.x – 13.x.

Quick Answer

Model output validation treats all generated responses from an AI system as completely untrusted input. Large Language Models can hallucinate, be coerced into malicious outputs via indirect prompt injection, or generate syntax payloads designed to exploit downstream systems. Never pass LLM outputs directly into raw database queries, shell exec commands, eval functions, or unescaped Blade {!! $output !!} tags. Always validate schemas, type-cast fields, and escape data based on downstream context.

Threat Model

Vulnerable Example

[INSECURE] Vulnerable Pattern
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;

class VulnerableAiAssistantController extends Controller
{
    /**
     * [INSECURE]: Blindly trusting AI output in database queries and Blade rendering.
     */
    public function renderAiSummary(Request $request)
    {
        $response = Http::withToken((string) config('services.openai.key'))
            ->post('https://api.openai.com/v1/chat/completions', [
                'model' => 'gpt-4o-mini',
                'messages' => [['role' => 'user', 'content' => $request->input('query')]],
            ]);

        $rawOutput = (string) $response->json('choices.0.message.content');

        // Vulnerability 1: Passing AI output into raw SQL
        DB::statement("UPDATE user_notes SET summary = '{$rawOutput}' WHERE user_id = 1");

        // Vulnerability 2: Blade template rendering rawOutput via {!! $rawOutput !!} (XSS)
        return view('notes.summary', ['summary' => $rawOutput]);
    }
}

Secure Implementation

[SECURE] Hardened Pattern
<?php

declare(strict_types=1);

namespace App\Services\Ai;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Validator;
use InvalidArgumentException;
use RuntimeException;

final class SecureStructuredOutputService
{
    /**
     * [SECURE]: Uses JSON Schema Structured Outputs and validates schema strictly before use.
     *
     * @return array{sentiment: string, score: float, category: string, clean_summary: string}
     */
    public function analyzeCustomerFeedback(string $feedbackText): array
    {
        // Define strict JSON schema accepted from the LLM
        $jsonSchema = [
            'name' => 'feedback_analysis',
            'strict' => true,
            'schema' => [
                'type' => 'object',
                'properties' => [
                    'sentiment' => [
                        'type' => 'string',
                        'enum' => ['positive', 'neutral', 'negative'],
                    ],
                    'score' => [
                        'type' => 'number',
                        'description' => 'Confidence score between 0.0 and 1.0',
                    ],
                    'category' => [
                        'type' => 'string',
                        'enum' => ['billing', 'technical', 'sales', 'general'],
                    ],
                    'clean_summary' => [
                        'type' => 'string',
                        'description' => 'A 1-sentence safe plain text summary',
                    ],
                ],
                'required' => ['sentiment', 'score', 'category', 'clean_summary'],
                'additionalProperties' => false,
            ],
        ];

        $response = Http::withToken((string) config('services.openai.key'))
            ->timeout(15)
            ->post('https://api.openai.com/v1/chat/completions', [
                'model' => 'gpt-4o-mini',
                'messages' => [
                    ['role' => 'system', 'content' => 'Analyze the customer feedback text. Output only valid JSON following schema.'],
                    ['role' => 'user', 'content' => $feedbackText],
                ],
                'response_format' => [
                    'type' => 'json_schema',
                    'json_schema' => $jsonSchema,
                ],
            ]);

        if (!$response->successful()) {
            throw new RuntimeException('AI service request failed.');
        }

        $rawJson = (string) $response->json('choices.0.message.content');
        $decoded = json_decode($rawJson, true, 512, JSON_THROW_ON_ERROR);

        if (!is_array($decoded)) {
            throw new RuntimeException('Model response is not a valid JSON structure.');
        }

        // Validate the structure with Laravel Validator
        $validated = Validator::make($decoded, [
            'sentiment' => ['required', 'string', 'in:positive,neutral,negative'],
            'score' => ['required', 'numeric', 'between:0,1'],
            'category' => ['required', 'string', 'in:billing,technical,sales,general'],
            'clean_summary' => ['required', 'string', 'max:500'],
        ])->validate();

        // Strip any residual HTML tags before passing downstream
        $validated['clean_summary'] = strip_tags($validated['clean_summary']);

        return $validated;
    }
}

Blade Template Best Practice

When rendering AI-generated text in Laravel Blade views, never use unescaped {!! !!} tags:

[INSECURE] Vulnerable Pattern
{{-- [INSECURE]: Vulnerable to second-order XSS from hallucinated or injected HTML --}}
<div>{!! $aiSummary !!}</div>

{{-- [SECURE]: Automatically escaped by Blade using htmlspecialchars() --}}
<div>{{ $aiSummary }}</div>

{{-- [SECURE]: If Markdown is required, use Laravel's Str::markdown with safe options --}}
<div>
    {!! \Illuminate\Support\Str::markdown($aiSummary, [
        'html_input' => 'strip',
        'allow_unsafe_links' => false,
    ]) !!}
</div>

Laravel-Specific Notes

Checklist

Further Reading

AI Assistant Shortcuts
Ask AI about this topic
← Prev: PII Redaction Pipeline
Next: Secure Configuration & Environment →
UF

Curated by Umar Farooq

Senior Full-Stack & Security Engineer with 5+ years of production experience in Laravel, PHP, and modern web application security. Author of the Laravel AI Security Masterclass.