227 Php Developer Interview Questions & Answers

116 top • 13 Amazon • 21 Google • 10 Netflix • 7 Meta • 18 NVIDIA • 21 Apple • 21 Microsoft

Php Developer icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: September 3, 2026)

81. How do you prevent cross-site scripting when rendering user-controlled data in PHP?SecurityEasy

Question Details

Explain context-aware output encoding for HTML text and attributes, safe templating defaults, URL and JavaScript contexts, input validation, and Content Security Policy as defense in depth.

Short Interview Answer (30-60 seconds)

I prevent XSS by encoding untrusted data for its exact output context. I use htmlspecialchars() for HTML text and ordinary quoted attributes, validate URL schemes, avoid inline JavaScript data insertion, keep template auto-escaping enabled, and add a restrictive Content Security Policy as defense in depth.

Detailed Explanation

See the Code while reading this explanation.

This question asks how to stop information supplied by a visitor from becoming harmful instructions in another visitor's browser. The key is to handle the information safely at the place where it is displayed. Text, links, page properties, and browser instructions do not follow the same rules, so each needs suitable protection. Checking information when it enters the application is helpful, but that check cannot replace safe handling when displaying it. A complete answer should also cover safer page-building tools, extra browser restrictions, safe failure, careful records, and tests that prove the protection works.

Useful clarifying questions:

Useful Questions to Ask the Interviewer
  • In which contexts can user-controlled data appear: HTML text, attributes, URLs, JavaScript, CSS, or permitted HTML?
  • Does the application use plain PHP templates or a template engine with automatic escaping?
  • Are relative URLs or any schemes other than HTTPS required?
  • Must users be allowed to submit limited HTML formatting?
How do you prevent cross-site scripting when rendering user-controlled data in PHP? diagram
How to Explain It in an Interview

The main rule is to encode untrusted data when it is rendered, using the encoder required by that exact browser context. Input validation should enforce business rules, such as expected length, type, format, and allowed values, but filtering or validation alone is not complete XSS protection.

For HTML text and ordinary quoted attributes, I use htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'). It converts characters such as <, >, &, single quotes, and double quotes into text that the HTML parser will not interpret as markup. ENT_SUBSTITUTE replaces invalid byte sequences safely. Attribute values must remain quoted. This rule does not make dangerous attributes such as onclick, style, or srcdoc safe, so I do not place untrusted data in those contexts.

A template engine should keep automatic escaping enabled by default. Developers should avoid raw-output features. When raw output is genuinely required, the value must come from a narrowly controlled source or be processed by a maintained, allowlist-based HTML sanitizer. A custom regular expression, strip_tags(), or a blacklist is not a reliable HTML sanitizer.

URLs require semantic validation as well as HTML encoding. I parse the URL and allow only the schemes the application needs, commonly https and, when justified, http. I reject unexpected schemes such as javascript: and data:. I then apply HTML attribute encoding when placing the accepted URL in a quoted href or src attribute. Encoding by itself cannot make a dangerous URL scheme safe. If relative URLs are allowed, I validate them with a separate policy instead of treating them as absolute URLs.

For JavaScript, I avoid placing user-controlled values directly inside inline scripts, event-handler attributes, or dynamically generated code. A safer design is to place serialized data in a non-executable data element and let trusted external JavaScript read it. When PHP must serialize data for an HTML script element, I use json_encode() with JSON_THROW_ON_ERROR, JSON_HEX_TAG, JSON_HEX_AMP, JSON_HEX_APOS, and JSON_HEX_QUOT. I never build JavaScript source by concatenating quoted user strings, and I do not pass untrusted strings to eval(), new Function(), or HTML-generating DOM APIs.

For CSS contexts, I avoid inserting user-controlled values into style attributes or style blocks. CSS has different parsing rules, and HTML encoding is not sufficient. I prefer selecting from predefined server-side values or applying a strict allowlist for a narrowly defined property.

Content Security Policy, or CSP, is a browser-enforced restriction on which scripts may run. A strong policy can block inline scripts, plugins, unsafe framing, and scripts from unapproved sources. Nonce-based or hash-based policies are stronger than allowing 'unsafe-inline'. CSP can reduce the impact of an encoding mistake, but it is defense in depth and never replaces context-aware output encoding.

When a value fails validation or cannot be encoded safely, the application should reject it or display a safe fallback instead of rendering the original value. Security logs can record the route, field name, validation reason, request identifier, and account identifier when appropriate. They should not contain passwords, session identifiers, authorization headers, private tokens, or unnecessarily complete attack payloads.

I verify the controls with automated tests containing HTML tags, closing tags, quote-breaking input, event handlers, dangerous URL schemes, malformed UTF-8, and strings containing </script>. I inspect the final browser DOM to confirm that values remain text or approved data, test the CSP response header, review CSP violation reports carefully, and use a security scanner as an additional check rather than as the only proof.

Key Insight / Why This Solution Works
  1. Inventory every location where user-controlled data is rendered.
  2. Classify each location as HTML text, ordinary quoted attribute, URL, JavaScript data, CSS, or permitted HTML.
  3. Validate the value against business rules and any context-specific allowlist.
  4. Prefer redesigning the page when the destination context is dangerous, such as an event-handler attribute, inline CSS, or executable JavaScript.
  5. Encode the value at output time with the correct context-specific method.
  6. Keep template automatic escaping enabled and restrict raw-output operations.
  7. Use a maintained allowlist-based sanitizer only when rendering user-authored HTML is an actual requirement.
  8. Add a restrictive CSP as defense in depth.
  9. Reject unsafe values or show a safe fallback, and log only non-secret diagnostic details.
  10. Test malicious boundary cases and inspect the final DOM and security headers.
Code
<?php

declare(strict_types=1);

function escapeHtml(string $value): string
{
    return htmlspecialchars(
        $value,
        ENT_QUOTES | ENT_SUBSTITUTE,
        'UTF-8'
    );
}

function validateAbsoluteHttpUrl(string $value): ?string
{
    if ($value === '' || strlen($value) > 2048) {
        return null;
    }

    $validated = filter_var($value, FILTER_VALIDATE_URL);
    if ($validated === false) {
        return null;
    }

    $scheme = strtolower((string) parse_url($validated, PHP_URL_SCHEME));
    if (!in_array($scheme, ['https', 'http'], true)) {
        return null;
    }

    return $validated;
}

function jsonForHtmlScript(mixed $value): string
{
    return json_encode(
        $value,
        JSON_THROW_ON_ERROR
        | JSON_HEX_TAG
        | JSON_HEX_AMP
        | JSON_HEX_APOS
        | JSON_HEX_QUOT
    );
}

$userName = (string) ($_GET['name'] ?? 'Guest');
$submittedUrl = (string) ($_GET['url'] ?? '');
$safeUrl = validateAbsoluteHttpUrl($submittedUrl);

$nonce = base64_encode(random_bytes(18));
header(
    "Content-Security-Policy: default-src 'self'; "
    . "script-src 'self' 'nonce-{$nonce}'; "
    . "object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
);

$pageData = [
    'displayName' => $userName,
];
?>
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Safe output example</title>
</head>
<body>
    <h1>Hello, <?= escapeHtml($userName) ?></h1>

    <?php if ($safeUrl !== null): ?>
        <a href="<?= escapeHtml($safeUrl) ?>" rel="noopener noreferrer">
            Visit submitted link
        </a>
    <?php else: ?>
        <p>The submitted link is not allowed.</p>
    <?php endif; ?>

    <script type="application/json" id="page-data"><?= jsonForHtmlScript($pageData) ?></script>
    <script nonce="<?= escapeHtml($nonce) ?>">
        const element = document.getElementById('page-data');
        const data = JSON.parse(element.textContent);
        console.log(data.displayName);
    </script>
</body>
</html>
Why Interviewers Ask This

The interviewer is testing whether the candidate understands that XSS prevention depends on where untrusted data is rendered. A strong answer should distinguish HTML text, attributes, URLs, JavaScript, and permitted HTML; explain safe PHP and template behavior; avoid relying on filtering alone; include defense in depth; and describe safe failure, logging, and verification.

Common interview mistakes

Common mistakes include treating input validation or filtering as complete protection; encoding data when it is stored instead of for its final output context; double-encoding or later decoding an already safe value; omitting ENT_QUOTES; leaving attributes unquoted; using HTML encoding inside event handlers, CSS, or JavaScript source; validating a URL's format but not its scheme; allowing javascript: or unnecessary data: URLs; inserting PHP values directly into JavaScript strings; using innerHTML when textContent is sufficient; using strip_tags(), a blacklist, or regular expressions as an HTML sanitizer; disabling template auto-escaping; marking untrusted content as raw; relying only on CSP; weakening CSP with broad sources or 'unsafe-inline'; and logging secrets or complete malicious payloads unnecessarily.

Interview tip

Start with one sentence: encode at output time for the exact browser context. Then distinguish HTML text, URLs, JavaScript, and permitted HTML. State clearly that validation and CSP are supporting controls, not substitutes for context-aware encoding.

Interviewer may ask next
What should you do when users are allowed to submit formatted HTML?

Use a maintained, allowlist-based HTML sanitizer configured to permit only the required elements, attributes, and URL schemes. Do not rely on strip_tags(), regular expressions, or a blacklist. Keep the allowlist small, sanitize at a clearly defined trust boundary, prevent later unsafe transformations, test known bypass patterns, and retain CSP as defense in depth.

Why is htmlspecialchars() not sufficient for every output context?

htmlspecialchars() is appropriate for HTML text and ordinary quoted attributes because it follows HTML parsing rules. It does not validate URL schemes and is not a JavaScript, CSS, or HTML-sanitization function. Other contexts use different parsers, so the application must use a context-specific encoder, strict allowlist, safe serializer, or a design that avoids placing untrusted data there.

82. What is cross-site request forgery (CSRF)?SecurityEasy

Question Details

Define CSRF as tricking a user browser into sending an unwanted state-changing request with credentials that the browser includes automatically. Explain the conditions required for the attack, CSRF tokens, SameSite cookies, origin checks, safe HTTP methods, re-authentication for sensitive actions, and why HTTPS or POST alone does not prevent CSRF.

Short Interview Answer (30-60 seconds)

CSRF tricks a signed-in user's browser into sending an unwanted state-changing request to a trusted site. Because the browser may automatically include session cookies, the site may accept it. Prevent it with CSRF tokens, SameSite cookies, origin checks, safe HTTP methods, and stronger confirmation for sensitive actions.

Detailed Explanation

A harmful website can sometimes make your browser perform an action on another website where you are already signed in. Your browser may automatically carry proof that you are signed in, so the second website may believe you chose the action yourself. For example, an attacker might try to make your browser change an account setting without your intention. The protection must make sure important actions really came from the website page the user was using. Several protections should work together because no single browser or network feature solves every case.

Useful Questions to Ask the Interviewer
  1. Should I explain CSRF protection for a traditional PHP application that uses session cookies?
  2. Should I cover both browser-level protections and server-side verification?
  3. Do you want an example of handling highly sensitive actions such as changing an email address or password?
What is cross-site request forgery (CSRF)? diagram
How to Explain It in an Interview

CSRF, or cross-site request forgery, happens when an attacker causes a user's browser to send an unwanted request to a site where that user is already authenticated. Authentication proves who the user is, but it does not prove that the user intentionally initiated that particular action. Authorization separately decides whether the authenticated user is allowed to perform the action. CSRF protection is needed because a request can be both authenticated and authorized while still not representing the user's intent.

A typical CSRF attack needs three important conditions. First, the application uses credentials that the browser sends automatically, commonly a session cookie. Second, the targeted endpoint changes server state, such as changing an email address, updating settings, or making a purchase. Third, the server accepts the request without requiring evidence that an attacker on another origin normally cannot provide.

For a traditional PHP application using cookie-based sessions, the main server-side defense is usually a CSRF token. The application generates a cryptographically unpredictable value and associates it with the user's session. Legitimate forms or application requests include that token. Before processing a protected state-changing request, the server checks that the submitted token matches the expected token. If it is missing or invalid, the application must reject the request before changing any data. In a custom PHP implementation, a token can be generated with random_bytes() and compared with hash_equals(). Frameworks commonly provide their own tested CSRF protection and should normally be used instead of rebuilding it.

The token works because an attacker can often cause the browser to send a request but normally cannot read pages or token values from another origin because of the browser's same-origin policy. If the application also has a cross-site scripting vulnerability, however, injected script running in the application's own origin may be able to read or submit CSRF tokens. CSRF protection therefore does not replace protection against cross-site scripting.

SameSite cookies provide another layer of protection. SameSite=Lax prevents cookies from being sent on many cross-site requests while still allowing some top-level navigations. SameSite=Strict is more restrictive and generally prevents the cookie from being sent during cross-site navigation, but it can interfere with legitimate flows from external sites. SameSite=None permits cross-site cookie use and requires the Secure attribute. SameSite is useful defense in depth, but the correct setting depends on the application's legitimate cross-site requirements and should not automatically replace server-side CSRF validation for important cookie-authenticated actions.

Origin checking is another useful layer. For state-changing requests, the server can verify that the Origin header matches an explicitly trusted origin. When Origin is unavailable, carefully validating Referer can be an appropriate fallback. The comparison should use an exact trusted scheme, host, and port policy rather than unsafe substring matching. Deployment details such as reverse proxies and canonical hostnames must also be handled correctly.

HTTP methods must have correct semantics. GET and HEAD are defined as safe methods and should not perform state-changing operations. Operations that create, update, or delete data should use an appropriate state-changing method such as POST, PUT, PATCH, or DELETE. However, simply changing an endpoint from GET to POST does not prevent CSRF. An attacker can often make a browser submit a cross-site POST form, and the browser may still attach applicable cookies automatically.

HTTPS also does not prevent CSRF. HTTPS protects the connection against network eavesdropping and modification, but CSRF uses the victim's own browser to create the request. The forged request can therefore be fully encrypted with HTTPS and still be unwanted.

For especially sensitive operations, such as changing a password, changing account recovery information, or confirming a financial action, the application can require re-authentication or another strong explicit confirmation in addition to normal CSRF defenses. This reduces the chance that possession of an existing authenticated session is enough to complete a high-impact action.

Protection should fail safely. If CSRF validation fails, return an appropriate error such as HTTP 403 and perform no state change. Log enough information to investigate repeated failures, such as the endpoint, time, and a request correlation identifier, but do not log passwords, session identifiers, CSRF tokens, or other secrets.

To verify the control, test a legitimate state-changing request with a valid token and confirm that it succeeds. Then send requests with a missing token, an incorrect token, and an unexpected Origin and confirm that every invalid request is rejected before any data changes. Also verify that GET and HEAD endpoints do not change application state and review the session cookie's SameSite, Secure, and HttpOnly settings.

Technical Approach
  1. Identify every endpoint that changes server state.
  2. Ensure safe methods such as GET and HEAD do not change state.
  3. For cookie-authenticated browser requests, generate or use the framework's cryptographically unpredictable CSRF token associated with the user's session.
  4. Include that token in legitimate state-changing forms or requests.
  5. Validate the token before performing the action.
  6. Configure session cookies with an appropriate SameSite policy and use Secure and HttpOnly where applicable.
  7. Validate Origin, with a carefully implemented Referer fallback where appropriate, as defense in depth.
  8. Require re-authentication or stronger confirmation for highly sensitive operations.
  9. Reject failures before any state change and log diagnostic information without secrets.
  10. Test valid, missing-token, invalid-token, and unexpected-origin cases.
Practical Insights

CSRF protection adds very little processing or memory cost. Creating or comparing a small token and checking request headers take constant work per request and use only a small amount of data. The larger cost is operational and maintenance work: every state-changing endpoint must consistently use the protection, cookie settings must match legitimate browser flows, and automated tests should prevent future endpoints from accidentally bypassing the controls.

Why Interviewers Ask This

Interviewers want to know whether the candidate understands why an authenticated browser can still be abused, what conditions make CSRF possible, and how to design layered protections instead of relying on weak assumptions such as HTTPS or POST requests alone. They also want practical judgment about tokens, cookies, request origins, HTTP methods, sensitive actions, safe failures, logging, and verification.

Common interview mistakes

Common mistakes are believing that authentication alone prevents CSRF, treating authorization as proof of user intent, using POST without CSRF validation, assuming HTTPS blocks forged requests, allowing GET requests to change data, using predictable tokens, accepting missing or invalid tokens, comparing origins with unsafe substring checks, treating SameSite as the only defense without considering application requirements, disabling protection for convenient endpoints, logging session identifiers or CSRF tokens, and changing application state before validation finishes.

Interview tip

Define the attack first, then explain the conditions that make it possible. Say clearly that authentication identifies the user but does not prove intent. Present CSRF tokens as the main application defense for traditional cookie-based sessions, then add SameSite cookies, origin checks, safe HTTP methods, and re-authentication as layered protections. Explicitly state that HTTPS and POST alone do not prevent CSRF.

Interviewer may ask next
Why does a CSRF token prevent a forged request?

A CSRF token is an unpredictable value that the trusted application provides to the legitimate user's page and verifies on a state-changing request. An attacker on another origin can often trigger a request but normally cannot read the application's token because of browser same-origin restrictions. The forged request therefore lacks the correct value and is rejected before state changes. An XSS vulnerability can weaken this protection because script running in the trusted origin may be able to access or submit the token.

If a session cookie uses SameSite=Strict, do we still need CSRF tokens?

SameSite=Strict provides strong browser-level protection because the cookie generally is not sent during cross-site requests or navigation. Whether a CSRF token can safely be omitted depends on the complete authentication design, supported browsers, application flows, and whether any credentials or endpoints behave differently. For important traditional cookie-authenticated applications, keeping server-side CSRF validation provides defense in depth and avoids relying on one browser control alone.

83. How do you protect a PHP form from cross-site request forgery?SecurityMedium

Question Details

Describe unpredictable per-session or per-request tokens, server-side validation, SameSite cookies, origin checks, token rotation, and why GET requests must not change state.

Short Interview Answer (30-60 seconds)

Generate an unpredictable CSRF token, store the expected value in the user's server-side session, include it in each state-changing form, and validate it with hash_equals(). Reject failures, use secure SameSite cookies and optional origin checks, rotate tokens when appropriate, and never change state through GET.

Detailed Explanation

See the Code while reading this explanation.

This question asks how to stop a harmful website from making a signed-in person unknowingly change information or perform an action. The main protection is to place a secret, hard-to-guess value in the real form and confirm it before accepting the change. Sign-in information alone is not enough because the person's browser may send it automatically. The application should reject unexpected submissions, avoid changing data through ordinary links, use extra browser protections, record failures without exposing secrets, and test that a fake submission cannot succeed.

Useful clarifying questions:

Useful Questions to Ask the Interviewer
  • Is this a server-rendered form, a cookie-authenticated API, or both?
  • Should tokens remain valid for a session or be single-use?
  • Does the application intentionally accept requests from other origins?
How do you protect a PHP form from cross-site request forgery? diagram
How to Explain It in an Interview

Cross-site request forgery, or CSRF, exploits the fact that a browser automatically attaches cookies to requests. Authentication identifies the signed-in session, but it does not prove that the user intended the action. Authorization must still confirm that the authenticated user is allowed to perform the requested operation.

For every cookie-authenticated request that changes state, normally POST, PUT, PATCH, or DELETE, I generate a cryptographically unpredictable token with random_bytes(). I store the expected value in the user's server-side session and place a copy in the legitimate form as a hidden field. For JavaScript requests, the client can send the token in a custom request header instead. On submission, the server requires the expected HTTP method, confirms that both tokens are non-empty strings, and compares them with hash_equals(), passing the trusted server-side value first. If validation fails, the application performs no state change and returns a generic 403 response.

A per-session token is simple and effective for many PHP applications. A per-request or single-use token reduces replay opportunities but requires more state and may break multiple tabs, retries, browser navigation, or forms that remain open for a long time. A practical design can rotate the token after authentication changes or sensitive operations. When strict single-use behavior is required, the server can retain a small bounded set of valid recently issued tokens and remove each token after successful use.

The session cookie should use Secure so it is sent only over HTTPS, HttpOnly so client-side scripts cannot read it, and an appropriate SameSite policy. SameSite=Lax is a practical default for many applications. SameSite=Strict provides stronger cross-site restrictions but can disrupt legitimate navigation or sign-in flows. SameSite=None is needed for some intentional cross-site uses and must be combined with Secure. SameSite is defense in depth and does not replace token validation.

An Origin check can provide another layer for state-changing requests. The server should compare the complete normalized scheme, host, and effective port against an exact allowlist. If Origin is absent, a carefully parsed Referer header may be checked as a fallback. The application must define how to handle requests with neither header because privacy tools, proxies, and some clients may omit them. The CSRF token remains the primary control.

GET and HEAD must remain safe and read-only. They must not create, update, delete, approve, purchase, log out, or otherwise change server-side state. This prevents links, images, crawlers, browser prefetching, and similar behavior from triggering an action. The server must enforce the method rule rather than relying on the user interface.

CSRF protection is mainly required when browsers automatically attach authentication credentials, especially cookies. An API that uses an Authorization header manually added by trusted client code is generally not exposed to classic form-based CSRF in the same way, although cross-origin policy, token storage, authorization, and other threats still require separate review.

A valid CSRF token does not replace input validation or authorization. After the CSRF check succeeds, the application must still validate submitted values and confirm that the signed-in user may perform the exact operation. Output encoding, parameterized queries, session security, and other controls address different threats and are not substitutes for CSRF protection.

Failures must be safe. The application should stop before any write occurs, return a generic error, and log only limited metadata such as the route, time, request identifier, failure category, and an internal user identifier when appropriate. It must not log the submitted token, expected token, session identifier, cookie values, passwords, or other secrets. Repeated failures can be monitored, while recognizing that expired forms, session expiry, and token rotation can also cause legitimate failures.

I would verify the protection with automated integration tests. The tests should cover a valid token, a missing token, a modified token, a token copied from another session, a stale or already-used token, an incorrect HTTP method, an untrusted Origin, an invalid Referer fallback, requests with neither origin header under the documented policy, multiple open tabs, expired sessions, and every state-changing route. I would also confirm that a request containing only the user's cookie cannot change state.

Key Insight / Why This Solution Works
  1. Require HTTPS and configure PHP sessions to use cookies only, strict session handling, Secure, HttpOnly, and an appropriate SameSite value.
  2. Generate a CSRF token with random_bytes() and store the expected value in server-side session state.
  3. Include the token in every state-changing HTML form as a hidden field, or in a custom header for JavaScript requests.
  4. Require POST, PUT, PATCH, or DELETE for state changes and reject unsupported methods.
  5. Read the submitted token and verify that both values are non-empty strings.
  6. Compare the submitted token with the trusted server-side token using hash_equals().
  7. Optionally validate Origin or a parsed Referer against an exact trusted-origin allowlist.
  8. On failure, perform no write, return HTTP 403, and log only non-secret metadata.
  9. After successful CSRF validation, perform normal input validation and authorization before changing data.
  10. Rotate tokens after authentication changes or sensitive operations according to the chosen usability policy.
  11. Test every state-changing route with valid, missing, invalid, cross-session, stale, replayed, wrong-method, and cross-origin cases.
Code
<?php
declare(strict_types=1);

const TRUSTED_ORIGINS = ['https://example.com'];

if (PHP_SAPI === 'cli') {
    fwrite(STDERR, "Run this example through a web server.\n");
    exit(1);
}

$isHttps = isset($_SERVER['HTTPS'])
    && $_SERVER['HTTPS'] !== ''
    && $_SERVER['HTTPS'] !== 'off';

if (!$isHttps) {
    http_response_code(500);
    header('Content-Type: text/plain; charset=UTF-8');
    echo 'HTTPS is required.';
    exit;
}

ini_set('session.use_only_cookies', '1');
ini_set('session.use_strict_mode', '1');
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_secure', '1');
ini_set('session.cookie_samesite', 'Lax');

session_start();

function failRequest(int $status, string $publicMessage, string $reason): never
{
    try {
        $requestId = bin2hex(random_bytes(8));
    } catch (Throwable) {
        $requestId = 'unavailable';
    }

    $path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);

    error_log(sprintf(
        'Request rejected: reason=%s request_id=%s path=%s',
        $reason,
        $requestId,
        is_string($path) ? $path : '/'
    ));

    http_response_code($status);
    header('Content-Type: text/plain; charset=UTF-8');
    echo $publicMessage;
    exit;
}

function normalizedOriginFromUrl(string $url): ?string
{
    $parts = parse_url($url);

    if (!is_array($parts) || !isset($parts['scheme'], $parts['host'])) {
        return null;
    }

    $scheme = strtolower((string) $parts['scheme']);
    $host = strtolower((string) $parts['host']);

    if ($scheme !== 'https' && $scheme !== 'http') {
        return null;
    }

    $port = isset($parts['port']) ? (int) $parts['port'] : null;
    $defaultPort = $scheme === 'https' ? 443 : 80;
    $portSuffix = $port !== null && $port !== $defaultPort ? ':' . $port : '';

    return $scheme . '://' . $host . $portSuffix;
}

function hasAllowedRequestOrigin(array $trustedOrigins): bool
{
    $originHeader = $_SERVER['HTTP_ORIGIN'] ?? '';

    if (is_string($originHeader) && $originHeader !== '') {
        $origin = normalizedOriginFromUrl($originHeader);
        return $origin !== null && in_array($origin, $trustedOrigins, true);
    }

    $refererHeader = $_SERVER['HTTP_REFERER'] ?? '';

    if (is_string($refererHeader) && $refererHeader !== '') {
        $origin = normalizedOriginFromUrl($refererHeader);
        return $origin !== null && in_array($origin, $trustedOrigins, true);
    }

    // This example allows missing headers because the CSRF token is primary.
    // A stricter application may reject them under a documented policy.
    return true;
}

if (!isset($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token'])) {
    try {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    } catch (Throwable) {
        failRequest(500, 'The request could not be completed.', 'token_generation_failed');
    }
}

$method = $_SERVER['REQUEST_METHOD'] ?? '';

if ($method === 'POST') {
    if (!hasAllowedRequestOrigin(TRUSTED_ORIGINS)) {
        failRequest(403, 'The request could not be completed.', 'untrusted_origin');
    }

    $expectedToken = $_SESSION['csrf_token'] ?? '';
    $submittedToken = $_POST['csrf_token'] ?? '';

    if (!is_string($expectedToken) || !is_string($submittedToken)) {
        failRequest(403, 'The request could not be completed.', 'invalid_token_type');
    }

    if ($expectedToken === '' || $submittedToken === '') {
        failRequest(403, 'The request could not be completed.', 'missing_token');
    }

    if (!hash_equals($expectedToken, $submittedToken)) {
        failRequest(403, 'The request could not be completed.', 'token_mismatch');
    }

    $displayName = $_POST['display_name'] ?? '';

    if (!is_string($displayName) || trim($displayName) === '') {
        failRequest(422, 'A valid display name is required.', 'invalid_input');
    }

    // Confirm that the authenticated user may perform this exact operation.
    // Persist the validated change only after authorization succeeds.

    try {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    } catch (Throwable) {
        failRequest(500, 'The request could not be completed.', 'token_rotation_failed');
    }

    header('Content-Type: text/plain; charset=UTF-8');
    echo 'Form submitted successfully.';
    exit;
}

if ($method !== 'GET') {
    header('Allow: GET, POST');
    failRequest(405, 'Method not allowed.', 'unsupported_method');
}

$escapedToken = htmlspecialchars(
    $_SESSION['csrf_token'],
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
);
?>
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Secure PHP Form</title>
</head>
<body>
<form method="post" action="">
    <input type="hidden" name="csrf_token" value="<?= $escapedToken ?>">
    <label>
        Display name
        <input type="text" name="display_name" required maxlength="100">
    </label>
    <button type="submit">Save</button>
</form>
</body>
</html>
Why Interviewers Ask This

The interviewer wants to verify that the candidate understands how a malicious site can cause a signed-in browser to send an unwanted request. A strong answer distinguishes authentication from user intent, uses server-validated unpredictable tokens, treats SameSite cookies and origin checks as additional layers, covers every state-changing endpoint, fails safely, and explains verification and token-lifecycle tradeoffs.

Common interview mistakes

Common mistakes include treating authentication as proof of user intent; relying only on SameSite, CAPTCHA, JavaScript, a custom field name, or a predictable hidden value; validating a token without a trusted server-side expected value; storing both token copies only in client-controlled data; using GET for state changes; protecting the visible form but not its processing endpoint; omitting alternate state-changing routes; using substring matching for origins; accepting another session's token; rotating tokens without considering multiple tabs; performing writes before validation finishes; forgetting authorization after CSRF validation; and logging tokens, cookies, or session identifiers.

Interview tip

Explain the threat first: browsers send cookies automatically, so authentication does not prove intent. Then describe the server-side token flow, safe rejection, SameSite and origin checks as extra layers, the rule that GET must not change state, the token-rotation tradeoff, and the tests you would run.

Interviewer may ask next
Is a SameSite cookie enough to prevent CSRF without a token?

No. SameSite limits many cross-site cookie uses, but intentional cross-site flows, SameSite=None cookies, client differences, and configuration mistakes can reduce its protection. For cookie-authenticated state changes, an unpredictable token validated against trusted server-side state should remain the primary control, with SameSite used as defense in depth.

Should a PHP application use one CSRF token per session or a new token for every request?

A per-session token is simpler, uses little additional state, and is usually effective when generated securely and replaced after authentication changes. A single-use token provides stronger replay resistance but complicates multiple tabs, retries, browser navigation, and long-lived forms. Sensitive applications can rotate after successful actions or keep a small bounded set of recent valid tokens.

84. What is the difference between authentication and authorization?SecurityEasy

Question Details

Define authentication as proving who a user or service is and authorization as deciding which actions and resources that identity may access. Explain sessions or tokens, roles and permissions, object-level checks, least privilege, deny by default, enforcement on every request, and why a successful login never replaces authorization checks.

Short Interview Answer (30-60 seconds)

Authentication answers, "Who are you?" Authorization answers, "What are you allowed to do?" Login may create a session or token that represents the authenticated identity, but every protected request must still check whether that identity may perform the requested action on the requested resource.

Detailed Explanation

A secure application makes two separate decisions. First, it checks that a person or connected service really is who it claims to be. Second, it decides what that person or service is allowed to see or change. Passing the first check does not give unlimited access. A signed-in customer should not automatically see another customer's information, and a normal employee should not automatically perform administrator actions. The application should give only the access that is needed and refuse access when permission has not been clearly granted.

Useful Questions to Ask the Interviewer
  1. Should I explain this mainly for a normal PHP web application with user sessions, or also include API tokens?
  2. Do you want examples of role-based permissions and checks for access to individual records?
What is the difference between authentication and authorization? diagram
How to Explain It in an Interview

Authentication proves identity. For example, a PHP application may verify login credentials and then create a secure server-side session. An API may instead validate a token that represents an authenticated user or service. A session or token helps the application recognize the same authenticated identity on later requests, but it does not by itself grant permission to every resource or action.

Authorization happens after the identity is known. It decides whether that identity may perform a specific action on a specific resource. For example, a user may be authenticated but still be forbidden from deleting users, opening an administrator page, or reading another customer's order.

Roles and permissions can provide broad authorization rules. A role such as admin may have permissions that a normal user role does not have. However, role checks alone are often not enough. The application may also need an object-level check. For example, before returning order 123, the server should verify that the current user is allowed to read that specific order, not merely that the user is logged in.

Authorization should follow least privilege. Each identity should receive only the permissions it needs. It should also follow deny by default. If no authorization rule clearly allows an action, the request should be rejected.

The server must enforce authorization on every protected request. A previous successful login does not replace the authorization check. The application must not rely on hidden buttons, disabled controls, URLs that are difficult to guess, or checks performed only in browser code because a requester can send HTTP requests directly to the server.

Failure should be safe. If authentication is missing or invalid, reject access without exposing sensitive information. If authentication succeeds but authorization fails, refuse the protected action. Log useful security information such as the identity identifier, attempted action, requested resource identifier, result, and request correlation information when appropriate. Do not log passwords, session identifiers, access tokens, or other secrets.

To verify the controls, test both allowed and denied cases. Confirm that an unauthenticated requester cannot reach protected functionality, a lower-privileged authenticated user cannot perform a higher-privileged action, and one authenticated user cannot access another user's protected object merely by changing an identifier. Also verify that missing or unknown permission rules result in denial rather than accidental access.

Technical Approach
  1. Authenticate the requester and establish a trusted identity using a secure session or validated token.
  2. Identify the requested action and resource.
  3. Determine the permissions or policy that apply to that identity.
  4. Check broad authorization rules such as roles or explicit permissions.
  5. Perform an object-level authorization check when access depends on ownership, membership, tenancy, or another relationship to the resource.
  6. Deny the request when no rule explicitly allows it.
  7. Execute the protected action only after authorization succeeds.
  8. Fail safely and log useful security events without secrets.
  9. Test successful access, missing authentication, insufficient permission, and access to another user's protected object.
Practical Insights

Authentication and authorization add some work to every protected request. Session authentication may require reading session data, while token authentication requires validating the token. Authorization may require checking permissions and sometimes reading ownership, membership, or other resource information from storage. Memory use is usually small. The larger cost is maintenance: roles, permissions, and object-level rules must stay correct as the application changes. Centralized and reusable authorization policies make these rules easier to test and maintain.

Why Interviewers Ask This

Interviewers want to know whether the candidate understands that proving identity and granting access are separate security responsibilities. They also evaluate whether the candidate would enforce permissions on every protected request, apply least privilege and deny-by-default rules, perform object-level checks, fail safely, and verify that unauthorized users cannot access protected resources.

Common interview mistakes

Common mistakes include treating successful login as permission to access everything, checking only a user's role without checking the requested object, trusting authorization decisions made only in browser code, protecting a page while forgetting the underlying API endpoint, allowing access when no permission rule exists, checking authorization only once instead of on every protected request, and logging passwords, session identifiers, access tokens, or other secrets.

Interview tip

Start with the simple distinction: authentication proves identity; authorization decides permitted actions and resources. Then give one example of a logged-in user being denied access to another user's record. Mention least privilege, deny by default, server-side checks on every protected request, and object-level authorization.

Interviewer may ask next
Why is checking that a user is logged in not enough to protect an object such as an order?

Login proves only which user is making the request. It does not prove that the user may access a particular order. The server must also perform an object-level authorization check, such as confirming that the order belongs to the current user or that the user has an explicit permission to access it. Otherwise, an authenticated user could change an order identifier and attempt to access another user's data.

What should happen if an authenticated user requests an action for which no authorization rule explicitly grants permission?

The application should deny the action. This is the deny-by-default principle. Access should be granted only when a defined authorization rule clearly permits the authenticated identity to perform that action on that resource. The application should fail safely, avoid exposing sensitive information, and may log the denied attempt without recording passwords, access tokens, session identifiers, or other secrets.

85. How should passwords be stored and verified in PHP?SecurityEasy

Question Details

Explain password_hash, password_verify, modern adaptive algorithms, salts handled by the API, rehashing, and why encryption or fast hashes are inappropriate.

Short Interview Answer (30-60 seconds)

Use password_hash() to create an adaptive one-way hash and password_verify() to check it. Prefer PASSWORD_ARGON2ID when available, otherwise use PASSWORD_DEFAULT. PHP handles the salt. After successful verification, use password_needs_rehash() to upgrade hashes created with an older algorithm or cost.

Detailed Explanation

See the Code while reading this explanation.

Passwords should be saved so nobody can turn the saved value back into the original words. When a person signs in, the application should check the entered password against the saved protected value. The checking process should be deliberately slow enough to make large numbers of guesses costly, while remaining acceptable for normal users. The saved value should include everything needed for later checking and should be replaceable with stronger protection over time. Failed sign-ins should not reveal whether an account exists, and passwords must never appear in logs.

Useful clarifying questions:

Useful Questions to Ask the Interviewer
  • Is Argon2id available in the PHP build used in production?
  • Does the application already contain hashes created by an older method?
  • What login response-time and memory limits must the application meet?
How should passwords be stored and verified in PHP? diagram
How to Explain It in an Interview

Use PHP's built-in password hashing API instead of designing a custom format. Create a password hash with password_hash() and verify it with password_verify(). A password hash is a one-way derived value. It should be practical to calculate once during registration or login but expensive for an attacker to calculate repeatedly for many guesses.

Use PASSWORD_ARGON2ID when the PHP build supports Argon2id. Argon2id is an adaptive, memory-hard password-hashing algorithm. Adaptive means its cost can be increased as hardware becomes faster. Memory-hard means each guess requires a configured amount of memory as well as processor time, which makes large-scale offline cracking more expensive.

Argon2id availability depends on how PHP was built. The application can check whether PASSWORD_ARGON2ID is defined or whether 'argon2id' appears in password_algos(). If Argon2id is unavailable, use PASSWORD_DEFAULT. In PHP 8.4 and PHP 8.5, PASSWORD_DEFAULT remains an alias for bcrypt, but the alias may change in a future major PHP release.

Do not manually generate or store a separate salt. PHP creates a cryptographically secure random salt for every call to password_hash(). The returned string contains the algorithm identifier, salt, cost parameters, and derived value. Store that complete string without changing or truncating it. Because PASSWORD_DEFAULT may produce a different-length format in the future, a VARCHAR(255) or equivalent binary-safe text column is a practical storage choice.

At login, retrieve the stored hash and call password_verify($submittedPassword, $storedHash). Do not call password_hash() again and compare the two strings because a new random salt normally produces a different hash each time. Do not write a custom equality or timing-safe comparison routine; password_verify() reads the algorithm and parameters from the stored hash and performs verification in a timing-attack-safe way.

After password_verify() succeeds, call password_needs_rehash() with the application's current algorithm and options. It returns true when the stored hash does not use those settings. If rehashing is needed, call password_hash() using the password that was just successfully verified and update the stored value. The update should be performed safely, preferably with a conditional database update or transaction so concurrent successful logins do not overwrite a newer hash incorrectly.

Never store plaintext passwords. Do not use reversible encryption because anyone who obtains the decryption key could recover every password. Do not use MD5, SHA-1, or a single SHA-256 or SHA-512 operation. These general-purpose hashes are intentionally fast, so an attacker with a stolen database can test very large numbers of guesses. Adding a salt to a fast hash prevents simple precomputed-table reuse but does not provide the deliberate cost of an adaptive password-hashing algorithm.

Authentication and authorization are different. Password verification authenticates the user's identity. Authorization is the later decision about which records, pages, or actions that authenticated user may access. A correct password check must not automatically grant unrestricted access.

Use the same general response for an unknown account and a wrong password, such as "Invalid credentials." This reduces direct account-enumeration information. Keep the public response generic, but record suitable internal security events such as repeated failures, operational errors, and request identifiers. Never log the plaintext password, reset token, full stored hash, or other authentication secrets.

Password hashing mainly protects against offline attacks after stored hashes are stolen. It does not by itself stop online guessing. Apply rate limiting, temporary backoff, monitoring, and appropriate multi-factor authentication separately where the application's risk requires them.

Benchmark the chosen algorithm and options on production-like hardware. Higher Argon2id memory or time settings increase resistance to cracking, but they also increase login latency, memory consumption per concurrent request, and denial-of-service exposure. Do not copy aggressive settings from another system without measuring them. Choose the strongest settings that the real login workload and infrastructure can safely support.

Verify the implementation with automated tests. Confirm that the correct password succeeds, an incorrect password fails, hashing the same password twice produces different encoded hashes because of different salts, both hashes still verify, outdated settings trigger password_needs_rehash(), current settings do not, malformed hashes fail safely, and passwords never appear in application logs.

Key Insight / Why This Solution Works
  1. Choose PASSWORD_ARGON2ID when it is available in the production PHP build; otherwise choose PASSWORD_DEFAULT.
  2. Benchmark the selected algorithm and options on production-like hardware before fixing custom costs.
  3. During registration or a password change, call password_hash() and store the complete returned string.
  4. Do not create a manual salt, encrypt the password, or apply a fast general-purpose hash.
  5. During login, retrieve the stored hash and call password_verify() with the submitted password.
  6. Return the same general failure message for an unknown account and an incorrect password.
  7. After successful verification, call password_needs_rehash() using the current algorithm and options.
  8. If rehashing is required, generate a replacement hash and update it safely in the database.
  9. Create the authenticated session and perform authorization as separate steps.
  10. Apply rate limiting and monitoring, and log failures without passwords, hashes, or other secrets.
  11. Test successful verification, rejection, unique salts, rehash detection, malformed input, concurrency behavior, and acceptable latency and memory use.
Code
<?php

declare(strict_types=1);

/**
 * @return array{algorithm: string|int, options: array<string, int>}
 */
function passwordConfiguration(): array
{
    if (defined('PASSWORD_ARGON2ID')) {
        return [
            'algorithm' => PASSWORD_ARGON2ID,
            'options' => [],
        ];
    }

    return [
        'algorithm' => PASSWORD_DEFAULT,
        'options' => [],
    ];
}

function createPasswordHash(string $plainPassword): string
{
    $configuration = passwordConfiguration();

    return password_hash(
        $plainPassword,
        $configuration['algorithm'],
        $configuration['options']
    );
}

/**
 * @return array{verified: bool, replacementHash: ?string}
 */
function verifyPassword(string $plainPassword, string $storedHash): array
{
    if (!password_verify($plainPassword, $storedHash)) {
        return [
            'verified' => false,
            'replacementHash' => null,
        ];
    }

    $configuration = passwordConfiguration();
    $replacementHash = null;

    if (password_needs_rehash(
        $storedHash,
        $configuration['algorithm'],
        $configuration['options']
    )) {
        $replacementHash = createPasswordHash($plainPassword);
    }

    return [
        'verified' => true,
        'replacementHash' => $replacementHash,
    ];
}

// Registration or password-change example.
$storedHash = createPasswordHash('Correct Horse Battery Staple!');

// Login example. In production, retrieve this hash from the account record.
$result = verifyPassword('Correct Horse Battery Staple!', $storedHash);

if (!$result['verified']) {
    // Use the same public response for an unknown account and a wrong password.
    echo "Invalid credentials.\n";
    exit;
}

if ($result['replacementHash'] !== null) {
    // Persist this with a parameterized and concurrency-safe database update.
    $storedHash = $result['replacementHash'];
}

// Session creation and authorization checks happen separately.
echo "Password verified.\n";
Why Interviewers Ask This

Interviewers want to confirm that the candidate understands the risks of a stolen password database and can use PHP's built-in password API correctly. The answer should show sound judgment about adaptive hashing, automatic salts, secure verification, algorithm availability, gradual rehashing, safe login failures, logging, performance tuning, and the difference between authentication and authorization.

Common interview mistakes

Common mistakes include storing plaintext passwords; using reversible encryption; using MD5, SHA-1, or a single SHA-256 or SHA-512 hash; adding a manual salt; truncating the encoded hash in the database; hashing the login input again and comparing strings; writing a custom verification comparison instead of using password_verify(); assuming Argon2id is available in every PHP build; hard-coding expensive Argon2id settings without benchmarking memory and concurrency; forgetting password_needs_rehash(); rehashing before the old password has been successfully verified; logging passwords or complete hashes; revealing whether an account exists; relying on hashing alone to stop online guessing; and treating authentication as authorization.

Interview tip

Start with password_hash(), password_verify(), and password_needs_rehash(). Explain that Argon2id is preferred when available and PASSWORD_DEFAULT is the portable fallback. Then cover automatic salts, why encryption and fast hashes are unsafe, safe failures, rehashing, benchmarking, logging without secrets, rate limiting, and the separation of authentication from authorization.

Interviewer may ask next
When should password_needs_rehash() be used?

Call it only after password_verify() succeeds, using the application's current algorithm and options. If it returns true, hash the successfully verified password again and safely replace the stored hash. This upgrades active accounts without storing plaintext passwords or forcing an immediate reset.

Should a PHP application add its own salt or pepper?

Do not add a manual salt because password_hash() generates a secure random salt and includes it in the encoded hash. A separately stored pepper may add protection in a specific threat model, but it introduces secret storage, rotation, availability, and recovery risks. It is optional defense in depth and never replaces an adaptive password hash.

86. How do prepared statements prevent SQL injection in PHP?SecurityEasy

Question Details

Explain placeholders and parameter binding, show why string concatenation is dangerous, and identify cases such as table or column names that still require allowlisting.

Short Interview Answer (30-60 seconds)

Prepared statements separate the SQL structure from user-supplied values. PHP sends values through placeholders, so the database treats them as data rather than executable SQL. They protect values only; dynamic table names, column names, and sort directions must be selected from strict allowlists.

Detailed Explanation

See the Code while reading this explanation.

This question asks how a PHP application can safely use information entered by a person when reading or changing stored records. The key idea is to keep the application's instruction separate from the entered information. If both are joined into one text string, a harmful entry may change the intended action. If they are supplied separately, the entry is handled only as information. Some choices, such as which field should control sorting, cannot be separated in the same way. Those choices must be limited to a fixed set approved by the developer.

Useful clarifying questions:

Useful Questions to Ask the Interviewer
  • Should I demonstrate PDO, MySQLi, or both?
  • Should I cover dynamic columns, tables, and sort directions?
  • Which database driver should I assume?
How do prepared statements prevent SQL injection in PHP? diagram
How to Explain It in an Interview

SQL injection occurs when untrusted input is inserted directly into an SQL string and is then interpreted as part of the SQL command. An attacker may use quotes, operators, comments, or additional SQL syntax to change the query's intended meaning.

A prepared statement separates:

  1. The fixed SQL structure, such as SELECT id FROM users WHERE email = :email.
  2. The data value supplied for the placeholder.

With PDO, PHP prepares the SQL statement and supplies the parameter value separately. When native prepared statements are used, the database receives the statement structure separately from its parameter values. The bound value is handled according to the placeholder position and parameter type. SQL-looking characters inside the value remain part of that value instead of becoming new SQL syntax.

Unsafe string concatenation looks like this:

$sql = "SELECT id FROM users WHERE email = '" . $email . "'";

If $email contains crafted input, it may terminate the quoted value and alter the SQL command.

The safe form uses a placeholder:

$statement = $pdo->prepare('SELECT id FROM users WHERE email = :email'); $statement->bindValue(':email', $email, PDO::PARAM_STR); $statement->execute();

PDO can emulate prepared statements for some drivers. Setting PDO::ATTR_EMULATE_PREPARES to false requests native preparation where the driver supports it. Correct parameterization remains essential either way, but native preparation more clearly preserves separation between the statement and its values and avoids some driver-specific emulation behavior.

Prepared-statement placeholders represent data values. They do not represent SQL identifiers or structural keywords. Therefore, a placeholder cannot safely choose a table name, column name, operator, or sort direction. For example, ORDER BY :column does not turn the supplied value into an identifier. The database generally treats it as a value, producing incorrect behavior rather than safely selecting a column.

For dynamic identifiers, map an external choice to a fixed developer-controlled value:

$allowedSortColumns = ['name' => 'name', 'created' => 'created_at'];

After confirming that the requested key exists, insert only the mapped constant into the SQL. Apply the same approach to table names and keywords such as ASC and DESC. Do not accept arbitrary identifier text and attempt to make it safe with filtering alone.

Input validation is still useful for business rules, such as requiring a valid email format or an integer within an expected range. However, validation is not a replacement for parameterized queries. A value can be valid for the application and still contain characters that would be dangerous if concatenated into SQL.

Use a least-privileged database account so the application can perform only the operations it requires. On failure, return a generic response to the client. Log an internal error code and safe diagnostic context, but do not log passwords, connection strings, session identifiers, access tokens, or unnecessary sensitive parameter values.

To verify the control, test normal values and hostile-looking values containing apostrophes, quotes, SQL comments, operators, and keywords. Confirm that each input is treated as one data value, does not change the query structure, and does not expose database error details. Also test that every unapproved identifier or sort direction is rejected before the SQL statement is built.

Key Insight / Why This Solution Works
  1. Write the SQL with placeholders for every untrusted data value.
  2. Prepare the statement with PDO or MySQLi.
  3. Bind each value using the appropriate parameter type, or pass values separately to execute.
  4. Never concatenate untrusted values into the SQL structure.
  5. For identifiers or SQL keywords that cannot use placeholders, map external choices to fixed values from a strict allowlist.
  6. Execute with a least-privileged database account.
  7. Return generic failure messages and log only non-sensitive diagnostic information.
  8. Test normal, malformed, and malicious-looking values, and verify that rejected identifiers never reach SQL construction.
Code
<?php

declare(strict_types=1);

header('Content-Type: application/json; charset=utf-8');

$email = $_GET['email'] ?? '';
$requestedSort = $_GET['sort'] ?? 'created';
$requestedDirection = strtolower($_GET['direction'] ?? 'desc');

if (!is_string($email) || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
    http_response_code(400);
    echo json_encode(['error' => 'Invalid request.'], JSON_THROW_ON_ERROR);
    exit;
}

$allowedSortColumns = [
    'name' => 'name',
    'created' => 'created_at',
];

$allowedSortDirections = [
    'asc' => 'ASC',
    'desc' => 'DESC',
];

if (!array_key_exists($requestedSort, $allowedSortColumns)
    || !array_key_exists($requestedDirection, $allowedSortDirections)) {
    http_response_code(400);
    echo json_encode(['error' => 'Invalid request.'], JSON_THROW_ON_ERROR);
    exit;
}

$sortColumn = $allowedSortColumns[$requestedSort];
$sortDirection = $allowedSortDirections[$requestedDirection];

$dsn = getenv('APP_DATABASE_DSN');
$username = getenv('APP_DATABASE_USER');
$password = getenv('APP_DATABASE_PASSWORD');

if ($dsn === false || $username === false || $password === false) {
    error_log('Database configuration is unavailable.');
    http_response_code(500);
    echo json_encode(['error' => 'Unable to process the request.'], JSON_THROW_ON_ERROR);
    exit;
}

try {
    $pdo = new PDO(
        $dsn,
        $username,
        $password,
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false,
        ]
    );

    $sql = "SELECT id, name, email, created_at
            FROM users
            WHERE email = :email
            ORDER BY {$sortColumn} {$sortDirection}";

    $statement = $pdo->prepare($sql);
    $statement->bindValue(':email', $email, PDO::PARAM_STR);
    $statement->execute();

    echo json_encode(
        ['users' => $statement->fetchAll()],
        JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
    );
} catch (PDOException $exception) {
    error_log('Database operation failed with code: ' . $exception->getCode());
    http_response_code(500);
    echo json_encode(
        ['error' => 'Unable to process the request.'],
        JSON_THROW_ON_ERROR
    );
} catch (JsonException $exception) {
    error_log('JSON encoding failed.');
    http_response_code(500);
    echo '{"error":"Unable to process the request."}';
}
Why Interviewers Ask This

Interviewers want to verify that the candidate understands the cause of SQL injection, can use PDO or MySQLi parameter binding correctly, does not rely on filtering or manual escaping as the primary defense, and recognizes that dynamic SQL identifiers require strict allowlisting because value placeholders cannot represent them.

Common interview mistakes

Common mistakes include concatenating or interpolating untrusted input into SQL, manually surrounding placeholders with quotes, treating input filtering or escaping as a complete defense, assuming validated data is safe to concatenate, attempting to bind table names or column names as parameters, accepting arbitrary ASC or DESC text, using an allowlist check but then inserting the original unchecked value, exposing database exception messages to clients, logging sensitive parameter values, and granting the application database account unnecessary permissions. Another mistake is claiming that prepared statements automatically secure SQL fragments that are still built through unsafe string concatenation.

Interview tip

Lead with the core rule: placeholders separate data values from SQL structure. Show one unsafe concatenation example and one parameterized PDO example. Then state the important limitation that identifiers and keywords cannot be bound and require fixed allowlists. Finish with validation, least privilege, safe failures, and a concrete verification test.

Interviewer may ask next
Can prepared-statement placeholders be used for table names, column names, or sort directions?

No. Placeholders represent data values, not SQL identifiers or structural keywords. Map each permitted external option to a fixed developer-controlled table name, column name, operator, or direction, reject every unknown option, and insert only the mapped constant into the SQL.

Do prepared statements make input validation and least-privileged database access unnecessary?

No. Prepared statements prevent values from changing the SQL structure, while validation enforces business rules such as format, range, and length. Least-privileged database access limits the damage possible from other defects or compromised application code. These controls serve different purposes and should be used together.

87. How should PHP sessions be hardened against fixation and hijacking?SecurityMedium

Question Details

Cover secure cookie flags, SameSite, TLS, session ID regeneration after authentication, strict mode, expiration, server-side invalidation, and avoiding sensitive data in identifiers.

Short Interview Answer (30-60 seconds)

I would require HTTPS, use cookie-only sessions with Secure, HttpOnly, and suitable SameSite settings, enable strict mode, regenerate the ID after authentication, enforce idle and absolute timeouts server-side, and invalidate sessions on logout or revocation. IDs must be random, opaque, and never placed in URLs or logs.

Detailed Explanation

This question asks how a website should protect the temporary pass that keeps a person signed in. An attacker may try to make the person use a pass already known to the attacker, or steal a valid pass after sign-in. The website should send it only through protected connections, prevent page scripts from reading it, replace it when trust increases, stop accepting it after suitable time limits, and cancel it fully when it is no longer valid. The pass must not contain personal or predictable information, and failures should safely require a new sign-in.

Useful clarifying questions:

Useful Questions to Ask the Interviewer
  • Is every application route available only through HTTPS?
  • Must the session work in cross-site login, embedded, or third-party flows?
  • Is session data stored locally or in shared storage across several servers?
  • Are concurrent browser requests common after login or privilege changes?
  • Must users be able to revoke other active sessions?
How should PHP sessions be hardened against fixation and hijacking? diagram
How to Explain It in an Interview

The two threats are session fixation and session hijacking. In fixation, an attacker causes a victim to use a session identifier that the attacker already knows. If the application keeps that identifier after login, the attacker may reuse it as an authenticated session. In hijacking, the attacker steals or otherwise obtains an already authenticated identifier and replays it.

Use TLS for the whole application, not only the login page. Redirect HTTP to HTTPS before creating a session, and configure HTTP Strict Transport Security at the trusted web server or reverse proxy. TLS protects the identifier while it travels over the network, but it does not prevent fixation, predictable identifiers, browser compromise, or application vulnerabilities.

Use cookies as the only session identifier transport. Enable session.use_only_cookies and disable transparent session ID propagation with session.use_trans_sid. Do not accept session IDs in URLs, query strings, form fields, or application-generated links. URL-based identifiers can leak through browser history, bookmarks, access logs, analytics, screenshots, copied links, and Referer headers.

Set the session cookie before session_start(). Use Secure so the browser sends it only over HTTPS. Use HttpOnly so ordinary browser JavaScript cannot read it. HttpOnly limits direct cookie theft through script access, but it does not make cross-site scripting harmless because injected code may still send authenticated requests from the victim's browser.

Set SameSite explicitly. SameSite=Lax is a practical default for many normal web applications because it blocks the cookie on many cross-site requests while allowing common top-level navigation. SameSite=Strict offers stronger isolation but may interrupt legitimate links or external sign-in flows. SameSite=None is appropriate only when cross-site cookie use is genuinely required, and it must be combined with Secure. SameSite is defense in depth and does not replace CSRF tokens or other CSRF controls for state-changing requests.

Prefer a host-only cookie by omitting the Domain attribute unless the application truly needs to share the session across subdomains. Use Path=/. A __Host- cookie name is useful when supported because it requires Secure, Path=/, and no Domain attribute. The Path attribute controls when the browser sends the cookie, but it is not an authorization boundary and should not be treated as protection from other applications on the same host.

Enable session.use_strict_mode. Strict mode makes PHP reject an uninitialized session ID supplied by the client and issue a newly generated ID instead. This prevents simple session adoption. When a custom session handler is used, confirm that it implements proper session ID validation; otherwise strict mode may not provide the expected protection.

Regenerate the session ID immediately after successful authentication and after any important privilege change, such as completing multi-factor authentication, entering an administrator mode, or changing to a more privileged role. The new ID prevents a pre-authentication identifier from becoming the authenticated credential. Do not regenerate before authentication and assume the problem is solved, because the important boundary is the change from lower trust to higher trust.

Concurrent requests require care. Immediately deleting old session data during regeneration can cause in-flight requests to lose state, while leaving the old identifier usable for too long creates a replay window. A production design should mark the old server-side session as obsolete, reject privileged use through it, allow only a short controlled transition when concurrent requests require one, and then remove it. The exact strategy depends on the session handler and request pattern. The application must never allow both identifiers to remain fully valid indefinitely.

Use both an idle timeout and an absolute timeout. The idle timeout expires the session after a period without legitimate activity. The absolute timeout ends it after a maximum total lifetime even if requests continue. Enforce both using timestamps stored and checked on the server. Do not rely only on the browser cookie lifetime, session.gc_maxlifetime, or garbage collection. Garbage collection controls cleanup of stored data and may run later; it is not a complete authorization-time expiration check.

When a session expires, invalidate it server-side before treating the request as unauthenticated. On logout, remove the server-side session state and expire the browser cookie using matching cookie attributes. Clearing only the cookie is insufficient if a copied identifier still maps to valid server-side data. For password reset, account recovery, suspected compromise, permission reduction, or an administrator-initiated revocation, invalidate the relevant sessions through a session registry, a per-user session version, or equivalent server-side control.

Use PHP's session identifier generation instead of constructing identifiers from user IDs, email addresses, roles, timestamps, sequential values, IP addresses, or hashes of predictable data. An identifier should be random, unguessable, and opaque. It should contain no sensitive information. Store the user identity and authorization-related state in protected server-side session data, and still perform authorization checks for each protected resource or action.

Do not treat IP-address or User-Agent binding as a primary defense. These values can change for legitimate users, may be shared, and can sometimes be copied or predicted by attackers. They may be used as risk signals for monitoring or reauthentication, but rigid binding can create false logouts and does not replace secure identifiers, TLS, expiration, or revocation.

Fail safely. If session startup, storage, validation, regeneration, expiration checking, or invalidation fails, do not continue as an authenticated user. Deny protected access and require a fresh sign-in. Show the user a generic message and record a sanitized security or operational event.

Log session creation, authentication, regeneration, expiration, logout, and administrative revocation when useful, but never log raw session IDs, Cookie headers, credentials, or authentication secrets. Use an independent request or event identifier for correlation. A keyed and truncated diagnostic fingerprint may be used only when the organization has a justified need and appropriate access and retention controls.

Verify the controls rather than assuming configuration is correct. Inspect Set-Cookie responses for Secure, HttpOnly, SameSite, Path, and Domain behavior. Confirm that HTTP cannot establish or transmit a production session, URL-supplied identifiers are ignored, unknown client-supplied identifiers are rejected, the identifier changes after authentication and privilege elevation, obsolete identifiers cannot authorize requests, idle and absolute limits are enforced server-side, and logout or account-wide revocation makes copied cookies unusable.

Technical Approach
  1. Require HTTPS for every route and configure HSTS at the trusted edge.
  2. Before session_start(), configure cookie-only sessions, disable URL propagation, enable strict mode, and set Secure, HttpOnly, SameSite, Path, and Domain rules.
  3. Confirm that any custom session handler validates identifiers and supports the required expiration and revocation behavior.
  4. Start the session and validate its server-side status, creation time, last legitimate activity time, and revocation state.
  5. If the session is invalid, expired, obsolete, or revoked, deny authenticated access and invalidate it safely.
  6. After successful authentication or privilege elevation, regenerate the identifier and handle concurrent requests with a short, controlled server-side transition if necessary.
  7. Enforce idle and absolute expiration on every authenticated request instead of relying only on cookie expiry or garbage collection.
  8. Store identity and authorization-related state only on the server, and perform authorization checks for every protected action.
  9. On logout, password reset, account recovery, compromise, or administrative revocation, invalidate the affected server-side sessions and expire the matching cookie.
  10. Log security-relevant lifecycle events without recording identifiers or secrets.
  11. Test cookie attributes, fixation resistance, regeneration, timeout enforcement, concurrent-request behavior, logout, and account-wide revocation.
Practical Insights

Normal session checks use a small fixed number of values, such as the session status, creation time, last activity time, and revocation version. From the application's view, validating or updating one session is normally constant-time work, although actual latency depends on whether storage is a local file, database, cache, or remote service. Memory and storage grow with the number and size of active server-side sessions. A per-user session list can make revoking all sessions proportional to that user's active session count, while a per-user version can make request checks and broad revocation close to constant-time. Operational costs include shared-storage availability, cleanup, race-condition handling, monitoring, and testing. Shorter timeouts improve security but cause more sign-ins. Stronger SameSite settings may break valid cross-site flows. Extra session metadata uses little space per session but becomes meaningful at very large scale.

Why Interviewers Ask This

Interviewers use this question to test whether the candidate understands session fixation, session hijacking, secure cookie configuration, session identifier lifecycle management, expiration, revocation, safe failure behavior, and production verification. They also want to see whether the candidate can distinguish authentication, which establishes identity, from authorization, which must still be checked for each protected action.

Common interview mistakes

Common mistakes include enabling HTTPS only on the login page; accepting identifiers through URLs; leaving session.use_strict_mode disabled; assuming strict mode alone removes the need for regeneration; using a custom session handler that does not validate IDs; regenerating before login but not after authentication; keeping both old and new identifiers fully valid; deleting old session data without considering concurrent requests; omitting Secure or HttpOnly; using SameSite=None without Secure; treating SameSite as a replacement for CSRF protection; setting a broad Domain attribute without need; treating Path as a security boundary; relying only on cookie expiry or session.gc_maxlifetime; clearing only the browser cookie during logout; failing to revoke sessions after password reset or compromise; placing user details in identifiers; building custom predictable IDs; logging raw cookies or session IDs; rigidly binding sessions to IP addresses; and trusting a stored role without checking authorization on each protected action.

Interview tip

Structure the answer around the session lifecycle: secure transport and cookie settings, strict acceptance rules, regeneration at trust changes, server-side expiration, and complete revocation. Mention the concurrent-request tradeoff, explain that SameSite does not replace CSRF protection, and finish with tests proving that obsolete or copied identifiers cannot authorize requests.

Interviewer may ask next
Why enable session.use_strict_mode if the application already regenerates the ID after login?

The controls address related but different risks. Strict mode rejects an uninitialized identifier supplied by the client, preventing PHP from adopting a simple attacker-chosen ID. Regeneration replaces the identifier when trust changes, especially after authentication. Strict mode does not replace regeneration because an existing pre-authentication session may be valid, and regeneration does not by itself ensure that unknown supplied IDs are rejected before login.

How should regeneration be handled when the browser sends concurrent requests?

The application must prevent the old identifier from remaining fully usable while avoiding accidental loss of legitimate in-flight requests. A production design can mark the old server-side session as obsolete, reject authentication or privileged actions through it, allow only a very short controlled transition when required, and then remove it. The transition must be server-side, time-limited, auditable, and tested with the actual session handler and request pattern.

88. How would you securely implement file uploads in PHP?SecurityMedium

Question Details

Explain size limits, MIME and content validation, generated filenames, storage outside the web root, permissions, malware scanning, image re-encoding where appropriate, and safe download responses.

Short Interview Answer (30-60 seconds)

I treat every upload as untrusted. I limit request and file size, allow only required formats, validate actual content, generate a random name, store it outside the web root, restrict permissions, quarantine and scan it, re-encode suitable images, and authorize every download with safe headers.

Detailed Explanation

See the Code while reading this explanation.

A file upload lets a person send a picture or document to a website. The danger is that the file may be harmful, much larger than expected, or different from what its name suggests. A safe design accepts only the kinds of files the website truly needs, checks what the file actually contains, gives it a new random name, and keeps it away from public website files. It also limits who may open it, checks it for harmful content, records failures safely, and returns it in a way that does not make the browser run it unexpectedly.

Useful clarifying questions:

Useful Questions to Ask the Interviewer
  • Which file formats are required?
  • What are the maximum file size and image dimensions?
  • Are files private, shared with selected users, or public?
  • Must images preserve animation, transparency, metadata, or color profiles?
  • Which malware-scanning service and failure policy are available?
  • What retention, audit, and deletion requirements apply?
How would you securely implement file uploads in PHP? diagram
How to Explain It in an Interview

I start with the threats. An attacker may upload executable code, disguise one format as another, exploit a vulnerable parser, upload a decompression bomb, overwrite a file, exhaust disk, CPU, or memory, include a dangerous name, or retrieve another user's private file. I therefore use several independent controls:

  1. Reject oversized requests early. I align the web server's body limit, PHP's post_max_size, upload_max_filesize, max_file_uploads, and the application's own limit. When post_max_size is exceeded, PHP may provide an empty upload payload, so the application must handle that case. I also limit request time and total storage usage.
  1. Check PHP's upload result. I require UPLOAD_ERR_OK, reject partial or missing uploads, and verify is_uploaded_file() before moving the temporary file. I do not trust $_FILES['type'], because the client supplies it.
  1. Use a strict allowlist. I accept only formats the feature needs. The original extension and filename are display information, not security evidence. I detect a likely MIME type from the bytes with Fileinfo and then perform format-specific parsing. Fileinfo is one signal, not proof that a complex file is harmless.
  1. Apply format-specific limits. For images, I check dimensions before full decoding, then decode with a maintained image library. This reduces, but does not eliminate, parser risk. I reject extreme dimensions and unsupported features according to business requirements.
  1. Generate the storage name. I create a cryptographically random identifier with random_bytes() and derive the extension only from the validated allowlist. I never join a user-supplied filename to a filesystem path. A sanitized original name may be kept only as metadata for display or download.
  1. Quarantine outside the web root. I first move the file to private quarantine storage that cannot be requested directly by URL and cannot execute scripts. The application account receives only the permissions it needs. On Unix-like systems, restrictive modes such as 0700 for private directories and 0600 for files are useful, but deployment ownership, access-control lists, containers, and object-storage policies still need review.
  1. Scan before release. I run the organization's approved malware scanner with a timeout. Infection, timeout, scanner error, or scanner unavailability fails closed: the file remains unavailable. Scanning lowers risk but cannot guarantee that a file is safe, so storage isolation and authorization still matter.
  1. Re-encode images only when suitable. Decoding and writing a fresh JPEG or PNG can remove metadata and non-pixel content. It may also change quality, orientation, animation, transparency, color profiles, or accessibility-related information. It is not a universal sanitizer and does not apply to arbitrary documents.
  1. Separate authentication from authorization. Authentication establishes the user's identity. Authorization decides whether that user may upload for a particular resource and whether they may download that specific file. Random filenames and hard-to-guess URLs are not access control. Cookie-authenticated upload endpoints also need CSRF protection unless the architecture provides an equivalent same-origin defense.
  1. Publish atomically. I make the file available only after validation, scanning, transformation, and trusted metadata storage succeed. If any step fails, I remove temporary artifacts. In a distributed system, I would use transactional metadata, immutable object keys, and an explicit state such as quarantined, clean, or rejected.
  1. Serve downloads through an authorized endpoint. The request supplies only a server-generated identifier. The application loads trusted metadata, authorizes access, opens the recorded private object, and sends an allowlisted Content-Type, a safe Content-Disposition, X-Content-Type-Options: nosniff, an appropriate cache policy, and usually attachment for untrusted active formats. It never accepts an arbitrary local path or reflects raw header text.
  1. Fail and log safely. The user receives a generic rejection. Internal logs can include a correlation identifier, authenticated user identifier, file identifier, size, detected type, validation stage, and scanner outcome. Logs must exclude file contents, credentials, session tokens, secrets, and unnecessary local paths.
  1. Verify the controls. I test valid files, renamed executables, MIME mismatches, double extensions, zero-byte and partial uploads, request-limit overflow, malformed files, huge pixel dimensions, decompression bombs, path traversal names, duplicate names, scanner infection and outage cases, failed cleanup, unauthorized downloads, direct storage access, and response headers. I also patch PHP, Fileinfo data, image libraries, malware engines, and downstream parsers.
Key Insight / Why This Solution Works
  1. Authenticate the requester and authorize uploading for the target resource.
  2. Apply CSRF protection when cookie-based authentication is used.
  3. Enforce matching web-server, PHP, application, count, time, and quota limits.
  4. Require UPLOAD_ERR_OK and verify is_uploaded_file().
  5. Compare the actual size with the application limit.
  6. Detect the likely type from file bytes and compare it with a strict allowlist.
  7. Perform format-specific parsing and structural limits, including image dimensions before full decoding.
  8. Generate a random storage identifier and choose an extension from trusted validation results.
  9. Move the file into private quarantine outside the web root with least-privilege access.
  10. Malware-scan it with a timeout and fail closed.
  11. Re-encode supported images when the product permits the resulting changes.
  12. Atomically publish the approved file and trusted metadata; otherwise remove temporary artifacts.
  13. Return only a server-generated identifier.
  14. On download, authenticate, authorize the exact file, resolve only trusted metadata, and stream it with defensive headers.
  15. Log sanitized outcomes and test bypass, outage, cleanup, and resource-exhaustion cases.
Code
<?php
declare(strict_types=1);

session_start();

const MAX_UPLOAD_BYTES = 5_000_000;
const MAX_IMAGE_WIDTH = 6000;
const MAX_IMAGE_HEIGHT = 6000;
const STORAGE_ROOT = '/srv/private/php-upload-example';
const QUARANTINE_DIR = STORAGE_ROOT . '/quarantine';
const FILES_DIR = STORAGE_ROOT . '/files';
const META_DIR = STORAGE_ROOT . '/metadata';
const CLAMDSCAN_PATH = '/usr/bin/clamdscan';
const SCAN_TIMEOUT_SECONDS = 20;

final class HttpError extends RuntimeException
{
    public function __construct(public readonly int $status, string $message)
    {
        parent::__construct($message);
    }
}

function sendJson(int $status, array $body): never
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    header('Cache-Control: no-store');
    header('X-Content-Type-Options: nosniff');
    echo json_encode($body, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
    exit;
}

function requireUserId(): string
{
    $userId = $_SESSION['user_id'] ?? null;
    if (!is_string($userId) || !preg_match('/\A[A-Za-z0-9_-]{1,64}\z/', $userId)) {
        throw new HttpError(401, 'Authentication required');
    }
    return $userId;
}

function requireCsrfToken(): void
{
    $sessionToken = $_SESSION['csrf_token'] ?? null;
    $requestToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null;

    if (!is_string($sessionToken) || !is_string($requestToken) || !hash_equals($sessionToken, $requestToken)) {
        throw new HttpError(403, 'Request rejected');
    }
}

function ensurePrivateDirectories(): void
{
    foreach ([STORAGE_ROOT, QUARANTINE_DIR, FILES_DIR, META_DIR] as $directory) {
        if (!is_dir($directory) && !mkdir($directory, 0700, true) && !is_dir($directory)) {
            throw new RuntimeException('Private storage is unavailable');
        }
        if (DIRECTORY_SEPARATOR === '/' && !chmod($directory, 0700)) {
            throw new RuntimeException('Cannot secure private storage permissions');
        }
    }
}

function cleanDisplayName(string $name): string
{
    $name = preg_replace('/[\x00-\x1F\x7F]/u', '', $name) ?? '';
    $name = trim(str_replace(['/', '\\'], '_', $name));
    if ($name === '') {
        return 'upload';
    }
    return function_exists('mb_substr') ? mb_substr($name, 0, 120) : substr($name, 0, 120);
}

function validateImage(string $path): array
{
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mime = $finfo->file($path);

    $allowed = [
        'image/jpeg' => ['extension' => 'jpg', 'imageType' => IMAGETYPE_JPEG],
        'image/png' => ['extension' => 'png', 'imageType' => IMAGETYPE_PNG],
    ];

    if (!is_string($mime) || !isset($allowed[$mime])) {
        throw new HttpError(415, 'Upload rejected');
    }

    $info = @getimagesize($path);
    if ($info === false || $info[2] !== $allowed[$mime]['imageType']) {
        throw new HttpError(415, 'Upload rejected');
    }

    [$width, $height] = $info;
    if ($width < 1 || $height < 1 || $width > MAX_IMAGE_WIDTH || $height > MAX_IMAGE_HEIGHT) {
        throw new HttpError(422, 'Upload rejected');
    }

    return [
        'mime' => $mime,
        'extension' => $allowed[$mime]['extension'],
        'width' => $width,
        'height' => $height,
    ];
}

function scanForMalware(string $path): void
{
    if (!is_executable(CLAMDSCAN_PATH)) {
        throw new RuntimeException('Malware scanner is unavailable');
    }

    $process = proc_open(
        [CLAMDSCAN_PATH, '--fdpass', '--no-summary', $path],
        [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
        $pipes
    );

    if (!is_resource($process)) {
        throw new RuntimeException('Malware scanner could not start');
    }

    fclose($pipes[0]);
    stream_set_blocking($pipes[1], false);
    stream_set_blocking($pipes[2], false);

    $deadline = microtime(true) + SCAN_TIMEOUT_SECONDS;
    $exitCode = null;

    while (true) {
        $status = proc_get_status($process);
        if (!$status['running']) {
            $exitCode = $status['exitcode'];
            break;
        }
        if (microtime(true) >= $deadline) {
            proc_terminate($process, 9);
            break;
        }
        usleep(50_000);
    }

    stream_get_contents($pipes[1]);
    stream_get_contents($pipes[2]);
    fclose($pipes[1]);
    fclose($pipes[2]);

    $closeCode = proc_close($process);
    if ($exitCode === null || $exitCode < 0) {
        $exitCode = $closeCode;
    }

    if ($exitCode === 1) {
        throw new HttpError(422, 'Upload rejected');
    }
    if ($exitCode !== 0) {
        throw new RuntimeException('Malware scan failed');
    }
}

function reencodeImage(string $source, string $destination, string $mime): void
{
    if ($mime === 'image/jpeg') {
        $image = @imagecreatefromjpeg($source);
        if ($image === false) {
            throw new HttpError(422, 'Upload rejected');
        }
        $saved = imagejpeg($image, $destination, 90);
        imagedestroy($image);
    } elseif ($mime === 'image/png') {
        $image = @imagecreatefrompng($source);
        if ($image === false) {
            throw new HttpError(422, 'Upload rejected');
        }
        imagealphablending($image, false);
        imagesavealpha($image, true);
        $saved = imagepng($image, $destination, 6);
        imagedestroy($image);
    } else {
        throw new HttpError(415, 'Upload rejected');
    }

    if (!$saved || !is_file($destination) || filesize($destination) === 0) {
        @unlink($destination);
        throw new RuntimeException('Image processing failed');
    }
    if (DIRECTORY_SEPARATOR === '/' && !chmod($destination, 0600)) {
        @unlink($destination);
        throw new RuntimeException('Cannot secure stored file permissions');
    }
}

function writeMetadataAtomically(string $path, array $metadata): void
{
    $temporary = $path . '.' . bin2hex(random_bytes(6)) . '.tmp';
    $json = json_encode($metadata, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);

    if (file_put_contents($temporary, $json, LOCK_EX) === false) {
        throw new RuntimeException('Cannot save upload metadata');
    }
    if (DIRECTORY_SEPARATOR === '/' && !chmod($temporary, 0600)) {
        @unlink($temporary);
        throw new RuntimeException('Cannot secure metadata permissions');
    }
    if (!rename($temporary, $path)) {
        @unlink($temporary);
        throw new RuntimeException('Cannot publish upload metadata');
    }
}

function handleUpload(string $userId): never
{
    requireCsrfToken();

    if (!isset($_FILES['file']) || !is_array($_FILES['file'])) {
        throw new HttpError(400, 'Upload rejected');
    }

    $upload = $_FILES['file'];
    $error = $upload['error'] ?? UPLOAD_ERR_NO_FILE;
    if ($error !== UPLOAD_ERR_OK) {
        $status = in_array($error, [UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE], true) ? 413 : 400;
        throw new HttpError($status, 'Upload rejected');
    }

    $temporaryPath = $upload['tmp_name'] ?? '';
    $reportedSize = $upload['size'] ?? -1;
    if (!is_string($temporaryPath) || !is_int($reportedSize) || !is_uploaded_file($temporaryPath)) {
        throw new HttpError(400, 'Upload rejected');
    }

    $actualSize = filesize($temporaryPath);
    if ($actualSize === false || $actualSize < 1 || $actualSize > MAX_UPLOAD_BYTES || $reportedSize > MAX_UPLOAD_BYTES) {
        throw new HttpError(413, 'Upload rejected');
    }

    $validated = validateImage($temporaryPath);
    $id = bin2hex(random_bytes(16));
    $quarantinePath = QUARANTINE_DIR . '/' . $id . '.upload';
    $storedName = $id . '.' . $validated['extension'];
    $finalPath = FILES_DIR . '/' . $storedName;
    $metadataPath = META_DIR . '/' . $id . '.json';

    if (!move_uploaded_file($temporaryPath, $quarantinePath)) {
        throw new RuntimeException('Cannot quarantine upload');
    }
    if (DIRECTORY_SEPARATOR === '/' && !chmod($quarantinePath, 0600)) {
        @unlink($quarantinePath);
        throw new RuntimeException('Cannot secure quarantine permissions');
    }

    try {
        scanForMalware($quarantinePath);
        reencodeImage($quarantinePath, $finalPath, $validated['mime']);

        $storedSize = filesize($finalPath);
        if ($storedSize === false) {
            @unlink($finalPath);
            throw new RuntimeException('Cannot read stored file size');
        }

        writeMetadataAtomically($metadataPath, [
            'id' => $id,
            'owner' => $userId,
            'storedName' => $storedName,
            'displayName' => cleanDisplayName((string) ($upload['name'] ?? 'upload')),
            'mime' => $validated['mime'],
            'size' => $storedSize,
            'width' => $validated['width'],
            'height' => $validated['height'],
            'createdAt' => gmdate('c'),
        ]);
    } catch (Throwable $error) {
        @unlink($finalPath);
        @unlink($metadataPath);
        throw $error;
    } finally {
        @unlink($quarantinePath);
    }

    error_log(json_encode([
        'event' => 'upload_accepted',
        'fileId' => $id,
        'userId' => $userId,
        'mime' => $validated['mime'],
        'size' => $actualSize,
    ], JSON_UNESCAPED_SLASHES));

    sendJson(201, ['id' => $id]);
}

function contentDisposition(string $displayName): string
{
    $fallback = preg_replace('/[^A-Za-z0-9._-]/', '_', $displayName) ?: 'download';
    return 'attachment; filename="' . $fallback . '"; filename*=UTF-8\'\'' . rawurlencode($displayName);
}

function handleDownload(string $userId): never
{
    $id = $_GET['id'] ?? '';
    if (!is_string($id) || !preg_match('/\A[a-f0-9]{32}\z/', $id)) {
        throw new HttpError(404, 'File not found');
    }

    $json = @file_get_contents(META_DIR . '/' . $id . '.json');
    if ($json === false) {
        throw new HttpError(404, 'File not found');
    }

    try {
        $metadata = json_decode($json, true, 16, JSON_THROW_ON_ERROR);
    } catch (JsonException) {
        throw new RuntimeException('Stored metadata is invalid');
    }

    if (!is_array($metadata) || !is_string($metadata['owner'] ?? null) || !hash_equals($metadata['owner'], $userId)) {
        throw new HttpError(404, 'File not found');
    }

    $storedName = $metadata['storedName'] ?? '';
    $mime = $metadata['mime'] ?? '';
    if (!is_string($storedName) || !preg_match('/\A[a-f0-9]{32}\.(jpg|png)\z/', $storedName)) {
        throw new RuntimeException('Stored metadata is invalid');
    }
    if (!in_array($mime, ['image/jpeg', 'image/png'], true)) {
        throw new RuntimeException('Stored metadata is invalid');
    }

    $path = FILES_DIR . '/' . $storedName;
    $size = @filesize($path);
    $handle = @fopen($path, 'rb');
    if ($handle === false || $size === false) {
        throw new HttpError(404, 'File not found');
    }

    http_response_code(200);
    header('Content-Type: ' . $mime);
    header('Content-Length: ' . (string) $size);
    header('Content-Disposition: ' . contentDisposition((string) ($metadata['displayName'] ?? 'download')));
    header('X-Content-Type-Options: nosniff');
    header('Cache-Control: private, no-store');
    header("Content-Security-Policy: default-src 'none'; sandbox");

    fpassthru($handle);
    fclose($handle);
    exit;
}

try {
    ensurePrivateDirectories();
    $userId = requireUserId();

    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        handleUpload($userId);
    }
    if ($_SERVER['REQUEST_METHOD'] === 'GET') {
        handleDownload($userId);
    }

    throw new HttpError(405, 'Method not allowed');
} catch (HttpError $error) {
    sendJson($error->status, ['error' => $error->getMessage()]);
} catch (Throwable $error) {
    $correlationId = bin2hex(random_bytes(8));
    error_log(json_encode([
        'event' => 'file_operation_failed',
        'correlationId' => $correlationId,
        'exception' => $error::class,
    ], JSON_UNESCAPED_SLASHES));

    sendJson(500, [
        'error' => 'File operation failed',
        'correlationId' => $correlationId,
    ]);
}
Why Interviewers Ask This

Interviewers want to see whether the candidate understands that uploaded files are attacker-controlled input and can affect code execution, data confidentiality, storage, memory, CPU, and downstream parsers. The question tests layered validation, resource limits, filename safety, storage isolation, least privilege, malware scanning, image handling, authentication versus authorization, secure download responses, safe failure behavior, logging, and verification. It also reveals whether the candidate incorrectly trusts a filename extension, a browser-supplied content type, a random URL, or one scanner result as complete protection.

Common interview mistakes

Mistakes include trusting $_FILES['type'], checking only the extension, treating Fileinfo or malware scanning as a guarantee, accepting every format, using the original name as a path, allowing path traversal, storing files under the document root, permitting script execution, using predictable names, and treating an unguessable URL as authorization. Other errors are missing CSRF protection on cookie-authenticated uploads, checking compressed size but not image dimensions, decoding before resource checks, publishing before scanning finishes, accepting files when the scanner fails, passing user text through a shell, reflecting raw names into headers, serving active content inline, exposing internal paths, logging secrets or contents, ignoring cleanup failures, and failing to test direct storage access or unauthorized downloads.

Interview tip

Organize the answer as layers: limit, validate, isolate, scan, transform, publish, authorize, serve safely, fail closed, and verify. Explicitly say that extensions, MIME detection, random names, image re-encoding, and malware scanning are useful controls but none is complete protection alone.

Interviewer may ask next
Why are extension checks and MIME detection not enough to make an uploaded file safe?

They identify the likely format, but a valid-looking file can still contain malicious macros, scripts, embedded objects, malformed structures, or data that exploits a parser. I combine a strict allowlist and content detection with format-specific parsing, structural and resource limits, maintained libraries, malware scanning, isolation, safe transformation where appropriate, and authorization. I also keep every downstream parser patched because the file remains untrusted even after validation.

What should happen when malware scanning is slow or unavailable?

The file should remain in private quarantine and must not be downloadable or sent to downstream systems. The scan needs a bounded timeout. Infection, timeout, service error, or unavailability should fail closed. A workflow may retry asynchronously, but the file remains in a non-public quarantined state until a successful clean result is recorded. The system should remove expired quarantine objects, preserve only sanitized audit data, and alert operations when scanner failures, queue age, or capacity thresholds are exceeded.

89. How do you prevent insecure direct object reference vulnerabilities in a PHP API?SecurityMedium

Question Details

Explain object-level authorization on every request, tenant scoping, non-guessable identifiers as a secondary measure, avoiding trust in client ownership fields, and security testing.

Short Interview Answer (30-60 seconds)

I prevent IDOR by authorizing every action on every requested object. I scope database reads and writes with trusted tenant, owner, role, or relationship data from the authenticated server context. I never trust client ownership fields. UUIDs help reduce guessing, but authorization and cross-account security tests are still required.

Detailed Explanation

This question asks how an online service stops one person from viewing, changing, or deleting another person's information by changing an identifier in a request. Being signed in is not enough. The service must check permission for the exact record and action every time. Account and organization details used for that decision must come from the service, not from values supplied by the person making the request. Hard-to-guess identifiers can help, but they cannot replace permission checks. Failed attempts should reveal little, be recorded safely, and be tested across different accounts.

Useful clarifying questions:

Useful Questions to Ask the Interviewer
  • Is the API single-tenant or multi-tenant?
  • Are permissions based on ownership, roles, assignments, or sharing rules?
  • Must unauthorized and nonexistent objects produce the same response?
  • Are there nested, bulk, export, or file-download endpoints for the resource?
How do you prevent insecure direct object reference vulnerabilities in a PHP API? diagram
How to Explain It in an Interview

An insecure direct object reference, or IDOR, occurs when an API accepts an object identifier and performs an operation without verifying that the authenticated caller may perform that action on that specific object. In API security terminology, this is commonly treated as broken object-level authorization.

Authentication answers, "Who is calling?" Authorization answers, "May this caller perform this action on this object?" A valid session or access token proves identity, but it does not grant access to every record.

The main control is object-level authorization on every request. The API must check permission separately for reading, updating, deleting, downloading, exporting, attaching, or otherwise acting on a resource. A user who may view an object is not automatically allowed to edit or delete it.

I derive identity, tenant, and role information from trusted server-side authentication context. Depending on the API, that context may come from a validated session or a verified access token. I do not accept owner_id, user_id, account_id, or tenant_id from the request as proof of permission. A client may send such a value as business input only when the endpoint explicitly permits reassignment and separately authorizes that administrative action.

For a tenant-scoped resource, I include the trusted tenant boundary in the database operation. For example:

SELECT id, tenant_id, status, total FROM invoices WHERE id = :id AND tenant_id = :tenant_id

Both values should be bound through a parameterized query. Parameterized queries prevent SQL injection, but they do not themselves prevent IDOR; the authorization scope in the query is what protects the object boundary.

For user-owned data, the query may also require an owner condition. For shared or assigned data, authorization may require a membership, assignment, or access-control relationship. For example, access might be allowed only when the caller belongs to the object's tenant and has a matching assignment or an authorized role.

I apply the same scoping to writes:

UPDATE invoices SET status = :status WHERE id = :id AND tenant_id = :tenant_id

However, an affected-row count of zero does not always prove that the object was unauthorized or missing. Some databases or configurations report zero when the new value is identical to the existing value. Therefore, application logic must not use affected-row count alone as the authorization decision. The API should use a database-supported returning clause, a scoped existence check, or a transaction with a scoped locked read when it must distinguish successful authorization from no effective data change. The exact method depends on the database and consistency requirements.

Authorization should be centralized in a policy, authorization service, repository boundary, or framework-supported access-control layer. Centralization reduces inconsistent checks across controllers and endpoints. The policy should deny by default and allow access only when an explicit rule succeeds.

Nested resources need full relationship validation. For /projects/{projectId}/files/{fileId}, the API must verify that the file belongs to the authorized project and tenant. It is unsafe to authorize the project and then load the file globally only by fileId.

Bulk endpoints must authorize every object, not only the first object or the request as a whole. The API must define whether the operation is atomic. A safe design may reject the entire request if any object is unauthorized. Another design may process only authorized objects and report per-item results, but it must not reveal sensitive information about unauthorized objects.

Non-guessable identifiers, such as securely generated random identifiers or UUIDs, are only a secondary defense. They reduce simple enumeration, but identifiers may leak through URLs, logs, browser history, emails, analytics, shared links, or another compromised account. Sequential identifiers are not inherently insecure when authorization is correct, and random identifiers are not secure when authorization is missing.

Input validation remains useful for checking that an identifier has the expected format and size. It can reject malformed input early, but filtering or validation alone cannot prove that the caller owns or may access the object.

Failure behavior should be consistent and reveal as little as practical. Many APIs return the same 404 Not Found response for nonexistent and unauthorized objects so that callers cannot confirm another object's existence. A 403 Forbidden response is reasonable when the API intentionally reveals that the object exists. The choice should follow the API contract and threat model.

Error responses should not expose database messages, owner details, tenant identifiers, policy internals, stack traces, or sensitive record data. JSON should be produced through the normal serializer rather than manual string construction. Output encoding is not the primary IDOR control, but safe serialization still prevents malformed responses and accidental data leakage.

I log denied access attempts with limited security-relevant context, such as a request correlation identifier, authenticated subject identifier, tenant identifier, resource type, attempted action, and result. I do not log passwords, access tokens, session cookies, authorization headers, secrets, or full sensitive objects. Logs must also be access-controlled and retained according to the application's security requirements.

If the API uses browser cookies for authentication, state-changing endpoints also need CSRF protection, such as an appropriate CSRF token strategy and suitable cookie settings. CSRF protection prevents another site from causing an authenticated request, but it does not replace object-level authorization. APIs using authorization headers rather than automatically attached browser credentials have a different CSRF risk profile.

Least privilege should also apply to service and database accounts. The PHP process should receive only the permissions it needs. This limits damage after another failure, but database privileges usually cannot express every per-user object rule, so application-level authorization is still required.

I verify the design with negative security tests. I create at least two users or tenants and confirm that one cannot read, update, delete, download, export, attach, or discover the other's objects. I test valid foreign identifiers, sequential guesses, leaked identifiers, nested resources, alternate HTTP methods, bulk requests, archived records, role changes, disabled users, shared resources, and administrator boundaries. I also test that denied writes cause no partial changes and that logs contain useful context without secrets.

Technical Approach
  1. Authenticate the request and build trusted server-side identity context.
  2. Validate the object identifier's syntax and size, without treating validation as authorization.
  3. Identify the exact action being requested, such as read, update, delete, export, or download.
  4. Determine the applicable tenant, ownership, role, assignment, sharing, and resource-state rules.
  5. Scope the database read or write with trusted authorization attributes whenever possible.
  6. Apply any additional policy checks that cannot be expressed safely in the query.
  7. Deny by default when no explicit rule grants the action.
  8. Return a consistent safe error that does not expose object or policy details.
  9. Record the denial with limited identifiers and no credentials, secrets, or sensitive object contents.
  10. Test cross-user, cross-tenant, nested-resource, bulk-operation, alternate-method, and race-condition cases.
Practical Insights

A properly indexed scoped lookup is normally close in cost to an ordinary object lookup. For tenant-scoped tables, an index such as (tenant_id, id) can help the database find the authorized record efficiently. More complex role, membership, or sharing rules may require joins or extra policy checks, so their cost depends on the data model and indexes. Memory use is usually small because the API should load only the required object and authorization data. Bulk requests can use more time and memory because every object must be authorized. The main maintenance cost is keeping policies consistent across all endpoints and testing them whenever permissions or resource relationships change.

Why Interviewers Ask This

Interviewers want to verify that the candidate distinguishes authentication from authorization and can enforce access rules for individual API objects. They also evaluate whether the candidate understands tenant isolation, ownership and role policies, safe database scoping, untrusted client fields, denial behavior, security logging, race conditions, and negative authorization testing.

Common interview mistakes

Common mistakes include checking only that the caller is authenticated; loading a record globally by identifier; trusting owner_id, user_id, or tenant_id from the client; enforcing permissions only in the user interface; and protecting reads while forgetting updates, deletes, exports, downloads, attachments, background jobs, or bulk endpoints. Other mistakes include treating UUIDs as authorization, validating only the parent resource, using affected-row count as the only proof of authorization, duplicating inconsistent checks across controllers, returning different errors that reveal object existence, logging tokens or sensitive records, failing to re-check permission after role changes, and testing only successful requests.

Interview tip

Start by distinguishing authentication from object-level authorization. Explain that trusted tenant, owner, role, or relationship data must scope every read and write. State that client ownership fields and UUIDs cannot prove access. Finish with safe denial behavior, limited security logging, and negative cross-account tests.

Interviewer may ask next
Are UUIDs enough to prevent IDOR vulnerabilities?

No. UUIDs make identifiers harder to guess, but they may still leak through URLs, logs, browser history, emails, analytics, shared links, or another account. The API must authorize every requested action on every object regardless of whether the identifier is sequential, random, or a UUID.

Should an API return 403 or 404 for an object that exists but belongs to another tenant?

The answer depends on the API contract and threat model. Returning the same 404 response for missing and unauthorized objects can reduce object-existence disclosure. Returning 403 is appropriate when confirming the object's existence is acceptable. Whichever behavior is chosen must be consistent, must not reveal ownership or policy details, and must not weaken the underlying authorization check.

90. How would you review a PHP deserialization path for security risks?SecurityHard

Question Details

Explain object injection through unserialize, magic methods, gadget chains, trusted formats, allowed_classes limitations, signatures, and safer data formats such as JSON.

Short Interview Answer (30-60 seconds)

I trace all data reaching unserialize(), determine who can modify it, and review loadable classes and magic methods for gadget chains. I prefer validated JSON. If legacy serialization must remain, I authenticate the bytes before parsing, restrict classes and depth, fail safely, isolate privileges, and test malicious payloads.

Detailed Explanation

This question asks how I would inspect a feature that rebuilds saved information inside a PHP application. The risk is that someone may change that information and make the application perform actions that were never intended. I would find where the information begins, who can change it, what the application rebuilds from it, and what actions can happen afterward. I would then remove the unsafe design where possible, place strict protections around any temporary legacy path, make failures harmless, and prove through testing that altered or harmful input is rejected.

Useful clarifying questions:

Useful Questions to Ask the Interviewer
  • Where does the serialized value come from: a request, cookie, session, cache, database, queue, file, or external service?
  • Can a user, another service, an administrator, or a compromised lower-trust system create or modify it?
  • Which application, framework, and Composer classes are available to the process through autoloading?
  • Must existing serialized records remain compatible, and for how long?
  • Does the payload require confidentiality, replay prevention, or only integrity and authenticity?
How would you review a PHP deserialization path for security risks? diagram
How to Explain It in an Interview

I would review the complete path from the data source to the possible security impact. I would not review only the visible unserialize() call.

First, I would locate every direct and indirect call to unserialize(). This includes wrappers, framework helpers, custom session handlers, cache adapters, queue consumers, migration scripts, background jobs, and imported legacy records. I would also inspect PHP configuration such as unserialize_callback_func and unserialize_max_depth because callbacks, autoloading, and depth settings affect the reachable behavior.

Next, I would trace each serialized value back to its true origin. Data is not trusted merely because it currently sits in a database, cache, session store, queue, or internal file. It may originally have come from a request, another service, an import, or a system with weaker permissions. I would determine whether an attacker can create, modify, replace, truncate, replay, or reorder the bytes before they reach unserialize().

Authentication answers who an identity is. Authorization decides what that identity may do. Neither control makes attacker-selected serialized PHP objects safe. Even an authenticated user may be allowed to submit data but must not be allowed to choose arbitrary object graphs for the server to rebuild.

The central threat is PHP object injection. PHP serialization can describe objects and their property values. During unserialization, PHP can load or resolve classes and can automatically invoke __unserialize() or __wakeup() when defined. The constructor is not the normal initialization path for a restored serialized object. Other magic methods, including __destruct(), __toString(), __get(), __set(), __call(), and __invoke(), may run later when the object is destroyed or used in a particular way.

A gadget is existing application or dependency code whose behavior can be misused. A gadget chain connects several methods so attacker-controlled object properties eventually cause a dangerous side effect. Possible sinks include file deletion or writing, command execution, dynamic callbacks, unsafe path use, server-side network requests, template processing, database changes, or disclosure of sensitive data. I would not claim a gadget chain exists until I confirm one in the exact deployed code and dependency versions.

I would inventory every class that the process can load, not only the classes intentionally used by the feature. That includes application classes, framework components, Composer packages, legacy libraries, and classes reachable through registered autoloaders or an unserialize callback. I would review magic methods and the methods they call, tracing attacker-controlled properties to side effects. Dependency changes matter because an update may add, remove, or alter a usable gadget.

My preferred fix is to avoid PHP object deserialization across a trust boundary. I would replace it with a versioned data-only format such as JSON and then explicitly construct approved domain objects from validated values. With json_decode(), I would normally request associative arrays and use JSON_THROW_ON_ERROR so malformed input does not become an ambiguous null result. I would set a suitable nesting-depth limit and enforce a maximum byte size before decoding.

JSON is safer for this purpose because decoding it does not automatically instantiate arbitrary application classes or invoke their magic methods. It is not automatically safe in every other respect. The decoded structure still needs strict validation of required keys, unexpected keys, types, string lengths, numeric ranges, allowed values, array counts, nesting, identifiers, and business rules. Large or deeply nested JSON can still consume CPU and memory, and later unsafe use of decoded values can create separate vulnerabilities.

The unserialize() allowed_classes option is only a risk-reduction control. Setting it to false prevents ordinary serialized classes from being instantiated and produces __PHP_Incomplete_Class objects for object records, while non-object values such as arrays and scalars may still be restored. An allowlist permits only named classes, but one allowed class may still be dangerous or may reach another useful gadget through its methods and properties. An allowlist can also become outdated after code or dependency changes. PHP's own security guidance is therefore not to pass untrusted input to unserialize(), regardless of allowed_classes.

The max_depth option and the unserialize_max_depth configuration limit can reduce excessive nesting and stack-exhaustion risk. They do not prevent object injection or dangerous magic-method behavior. I would set an application-appropriate positive limit rather than disabling the protection, but I would treat it only as defense in depth.

If a legacy serialized format cannot be removed immediately, I would accept it only from a source the application is designed to trust and authenticate the exact bytes before calling unserialize(). A common control is HMAC-SHA-256 with a strong purpose-specific secret key. I would calculate the expected tag over an unambiguous envelope containing the format version, purpose, metadata, and serialized bytes, then compare the expected and supplied tags with hash_equals(), placing the trusted expected value first and the supplied value second.

A plain hash is not sufficient because an attacker who changes the data can calculate another plain hash. Encryption without authentication is also insufficient because confidentiality does not prove that ciphertext or plaintext was not modified. When secrecy is required, I would use a well-reviewed authenticated-encryption construction or library that provides both confidentiality and integrity, with correct nonce handling.

A valid signature or authentication tag proves only that a holder of the key produced the authenticated bytes. It does not make a dangerous object graph harmless. It also does not protect against a compromised signer, a signing service that accepts attacker-selected serialized data, key theft, or a legitimate but vulnerable historical payload. The signer must construct the serialized value from trusted server-side state rather than sign arbitrary client-supplied bytes.

Authentication of the payload does not automatically prevent replay. When replay matters, the authenticated envelope should include a version, purpose, issuer, audience, creation time, expiration time, and a unique identifier or monotonic state. The application must validate those values before deserialization and store enough server-side state when one-time use is required. Timestamps alone do not guarantee one-time use.

Before parsing, I would reject payloads that exceed a strict byte limit or have an unknown version, purpose, encoding, or signature algorithm. I would verify authenticity before unserialize(), use the smallest possible allowed_classes list, set max_depth, and immediately validate the returned top-level type and all expected values. Because unserialize() can legitimately return false, I would not use a simple false result alone to distinguish valid serialized false from failure. I would convert warnings into controlled failures within the narrow parsing boundary or use explicit error handling that correctly distinguishes the expected value from malformed input.

I would minimize the classes and autoloaders available to the legacy conversion process. Where practical, I would run conversion in a separate worker or command with no shell capability, restricted filesystem permissions, blocked or tightly limited outbound network access, low-value credentials, bounded memory and execution time, and database permissions limited to the required records. Isolation does not make unsafe deserialization acceptable, but least privilege limits the impact if another control fails.

Failure behavior must be closed and predictable. An invalid tag, expired envelope, replayed identifier, unknown version, oversized input, excessive depth, disallowed class, incomplete class, malformed payload, warning, or throwable must stop the operation before any business action is committed. The application should return a generic error and must not expose serialized bytes, class names, paths, stack traces, secrets, or internal dependency details.

Logs should contain only useful security metadata, such as a correlation identifier, source category, payload size, format version, validation stage, and general rejection reason. They should not contain the raw payload, HMAC key, encryption key, authentication tag when unnecessary, session token, personal data, object property dump, or secret-bearing exception context. Repeated failures should be rate-limited or monitored where appropriate, without turning the parser into an oracle that reveals which internal check failed.

I would verify the design with unit, integration, and security tests. Cases should include unsigned data, one-byte modifications, incorrect and truncated tags, unknown versions, wrong purpose or audience, expired records, replay attempts, oversized payloads, excessive nesting, trailing data, malformed length fields, invalid class names, disallowed objects, __PHP_Incomplete_Class results, valid serialized false, and throwables from restoration methods. I would also test representative gadget-chain payloads only against the application's exact deployed dependency versions in an isolated test environment.

I would add static analysis or repository checks for new unserialize() calls and review changes to Composer dependencies, autoload configuration, magic methods, and allowed-class lists. Monitoring should detect unexpected increases in rejection counts, but alerts should not include sensitive payload data.

The long-term plan is to migrate away from native object serialization. A narrowly isolated converter can authenticate old records, deserialize them under strict controls, validate the resulting data, and write a versioned JSON representation. After all supported records are migrated and rollback requirements expire, I would remove the deserialization code, its keys, its class allowlist, and its special operational permissions.

Technical Approach
  1. Find every direct and indirect unserialize() call, wrapper, callback, session handler, cache adapter, queue consumer, migration script, and background job.
  2. Trace each serialized value to its original producer and identify every actor or system that can create, modify, replace, truncate, or replay it.
  3. Mark the trust boundary and identify all business actions and side effects that can occur after parsing.
  4. Inspect unserialize-related PHP options and configuration, including allowed_classes, max_depth, unserialize_callback_func, and autoload registration.
  5. Inventory all loadable application, framework, legacy, and Composer classes in the deployed environment.
  6. Review __unserialize(), __wakeup(), __destruct(), __toString(), property-access, invocation, and callback-related methods for paths from controllable properties to sensitive side effects.
  7. Confirm whether a real gadget chain exists in the exact deployed versions; do not infer one merely from the presence of a magic method.
  8. Prefer replacing native object serialization with a versioned data-only format such as JSON, strict byte and depth limits, schema validation, and explicit domain-object construction.
  9. For a temporary legacy path, authenticate an unambiguous envelope before parsing, validate purpose and freshness, use minimal allowed classes, set max_depth, and strictly validate the restored value.
  10. Apply safe failure handling, secret-free logs, bounded resources, restricted autoloading, process isolation, and least-privilege filesystem, network, database, and service permissions.
  11. Test malformed, modified, replayed, oversized, deeply nested, trailing-data, disallowed-class, incomplete-class, valid-false, throwable, and dependency-specific gadget cases.
  12. Monitor failures, review dependency changes, migrate existing records, and remove the legacy deserialization path.
Practical Insights

For an ordinary payload with no expensive application callbacks, reading and authenticating the bytes is approximately linear in payload size, written as O(n). Parsing also generally grows with the amount of serialized data and the number of restored values. Memory is approximately O(n) for the input plus the restored arrays, strings, and object graph, but duplicated strings and PHP value overhead can make actual memory use several times larger than the payload. Deep nesting can also consume call-stack or parser resources. Magic methods, autoloaders, callbacks, network calls, file operations, or database work can dominate both time and memory, so there is no safe runtime bound based only on payload length. Byte, depth, execution-time, and memory limits reduce denial-of-service risk. Maintaining class allowlists and reviewing dependency changes adds ongoing operational cost. Migrating to versioned JSON has an initial compatibility cost but normally lowers long-term security and maintenance risk.

Why Interviewers Ask This

This question tests whether the candidate can analyze a PHP deserialization path as a complete trust-boundary problem instead of treating unserialize() as an isolated function call. It evaluates knowledge of PHP object injection, magic-method execution, autoloading, dependency-based gadget chains, the limits of allowed_classes, integrity and replay controls, safer data-only formats, safe failure behavior, least privilege, operational monitoring, and practical verification. It also tests whether the candidate clearly distinguishes a preferred secure design from temporary controls needed for legacy compatibility.

Common interview mistakes

Specific mistakes include treating database, cache, queue, session, or internal-service data as automatically trusted; searching only for direct unserialize() calls; ignoring session handlers, wrappers, callbacks, and migration scripts; assuming constructors run during restoration; reviewing only __wakeup() and ignoring __unserialize() or later-triggered magic methods; claiming a vulnerability without proving a reachable gadget chain; ignoring Composer packages and autoloaders; believing allowed_classes or max_depth makes untrusted input safe; using an overly broad or stale class allowlist; authenticating the payload after parsing; using a plain hash; encrypting without authentication; allowing a signing service to sign arbitrary attacker-selected bytes; ignoring replay; using ambiguous concatenation in the signed message; failing to limit bytes or depth; treating a false return value alone as proof of failure; suppressing warnings without controlled handling; assuming JSON removes the need for schema and business validation; logging raw payloads, object dumps, tags, tokens, or keys; returning detailed parser errors; granting the conversion worker broad filesystem, network, database, or shell permissions; and testing only valid records instead of malformed and dependency-specific malicious cases.

Interview tip

Begin with the decision that untrusted PHP object data should not reach unserialize(). Then walk through source tracing, class and magic-method inventory, gadget-chain verification, the limits of allowed_classes and max_depth, pre-parsing authentication for temporary legacy data, replay controls, isolation, safe failures, and migration to validated JSON. Clearly separate risk reduction from a complete fix.

Interviewer may ask next
Are allowed_classes set to false and max_depth enough to make unserialize() safe for untrusted data?

No. allowed_classes set to false blocks ordinary class instantiation and restores serialized objects as __PHP_Incomplete_Class, while arrays and scalar values can still be parsed. max_depth limits nesting. These controls reduce particular risks, but they do not make malformed or attacker-controlled serialized data generally safe, prevent every resource-exhaustion case, or protect against later unsafe handling. PHP guidance is to avoid passing untrusted input to unserialize() and use a validated data-only format instead.

How would you protect a legacy serialized payload that cannot be migrated immediately?

I would allow it only from an intended trusted producer, place the format version, purpose, issuer, audience, timestamps, unique identifier, and exact serialized bytes in an unambiguous envelope, and authenticate that envelope before parsing with a purpose-specific key. I would compare tags with hash_equals(), enforce byte and depth limits, use the smallest class allowlist, validate the restored value, isolate the worker with least privilege, reject failures safely, test actual dependency-specific gadget payloads, and maintain a plan to migrate the data and remove unserialize().

More questions load as you scroll

Disclaimer: This interview guide is for educational and informational purposes only. It is designed to help readers prepare, but it does not guarantee any interview result, hiring decision, offer, or outcome. Interview questions, hiring criteria, and preferred answers can vary by employer, interviewer, industry, location, and time. The examples and explanations reflect the authors' research and judgment, are provided without warranties of any kind, and should not be treated as the only correct approach. Diagrams are simplified illustrations intended to highlight the main components and their interactions; actual systems and implementations may be more complex. Alternative approaches may be equally valid or better suited to a particular question, context, or interviewer. To the fullest extent permitted by applicable law, the author, contributors, and publisher are not liable for decisions made, actions taken, or losses incurred based on this guide.

Content Accuracy and Verification: To the fullest extent permitted by applicable law, we do not represent or warrant that interview guides, questions, answers, examples, or diagrams are accurate, complete, current, error-free, or suitable for any particular purpose. You are responsible for independently reviewing and verifying the information before relying on it.