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: August 28, 2026)

111. 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.

112. 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.

113. 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.

114. 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.

115. 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().

116. How would you respond to a compromised Composer dependency in a PHP application?SecurityHard

Question Details

Describe identifying affected versions, containment, lockfile and dependency analysis, secret rotation when needed, patching or replacing the package, testing, deployment, monitoring, and post-incident controls.

Short Interview Answer (30-60 seconds)

I would treat it as an incident, find every affected locked version and deployment, contain the package, preserve evidence, assess and rotate reachable secrets, patch or replace it, rebuild from trusted inputs, test and deploy safely, then monitor and improve dependency controls.

Detailed Explanation

This question asks what you would do when outside software used by a PHP application may have been changed or controlled by an attacker. You must explain how you would find every affected copy, stop further harm, preserve useful records, decide whether passwords or keys were exposed, replace the unsafe software, test the repaired application, release it safely, and watch for continuing problems. You should also explain how the team would prevent or detect a similar event sooner. The goal is to restore trust without assuming damage that has not been proven.

Useful clarifying questions:

Useful Questions to Ask the Interviewer
  • Which package, versions, release references, or time window are reported as compromised?
  • Is the compromise confirmed, and are an advisory, maintainer instructions, or indicators of compromise available?
  • Is the package a direct dependency, a transitive dependency, a Composer plugin, or a development-only dependency?
  • In which repositories, build artifacts, container images, hosts, and environments might it exist?
  • Where and when could its code have executed: during Composer operations, CI builds, web requests, workers, or command-line jobs?
  • Is there a verified fixed version, safe rollback version, maintained replacement, or approved temporary fork?
How would you respond to a compromised Composer dependency in a PHP application? diagram
How to Explain It in an Interview

I would treat the report as a security incident and assign clear ownership. I would pause risky deployments and package updates while establishing facts. I would not begin with an unrestricted composer update, because it can change many packages, complicate the investigation, and introduce unrelated differences.

First, I would confirm the affected package name, versions, source or distribution references, publication time, advisory details, and available indicators of compromise. I would obtain this information from trusted sources such as the package maintainer, Composer or Packagist security information, the affected source repository, and the organization's security team. A package name or version alone may be insufficient if an existing release archive was replaced or if only a specific commit or distribution artifact was affected.

Next, I would identify the exact exposure. composer.json describes acceptable version constraints, but composer.lock records the exact package versions and references selected for a particular application build. I would inspect both the packages and packages-dev sections of each lockfile. Useful read-only commands include composer show --locked, composer depends vendor/package --tree, and composer audit --locked. I would run Composer investigation commands in an isolated environment with plugins disabled when practical, because installed Composer plugins can execute during Composer commands. I would not treat a clean audit result as proof of safety because a new compromise may not yet appear in an advisory database.

I would search source repositories, archived lockfiles, CI records, software bills of materials, deployment manifests, release artifacts, container images, server inventories, and running environments for the affected package and reference. I would include older releases that could still be running or available for rollback. I would classify each occurrence as direct, transitive, production, development-only, build-time, Composer-plugin, or runtime use.

A development-only dependency is not automatically harmless. It may have executed in CI, tests, code generation, static analysis, or developer workstations, or it may have influenced an artifact later deployed to production. A Composer plugin is especially important because plugins can execute with the permissions of the account running Composer. Ordinary dependency-defined Composer scripts are not automatically executed by the root project; Composer executes scripts defined in the root package. However, compromised dependency code can still execute through plugins, autoloaded files, application imports, framework bootstrapping, test execution, command-line tools, or direct runtime calls.

I would contain the incident according to what the dependency could reach. Actions could include disabling the affected feature, stopping deployments, removing instances from service, pausing workers, blocking suspicious outbound destinations, restricting network egress, isolating CI runners, revoking package-repository credentials, or rolling back to a release independently verified as clean. If safe operation cannot be guaranteed, the application should fail closed by disabling the affected capability or rejecting related requests rather than continuing with untrusted code.

Before replacing systems, I would preserve relevant evidence. This can include composer.json, composer.lock, installed-package metadata, package archives, hashes, build and deployment logs, CI runner information, container image identifiers, filesystem timestamps, process information, network records, and relevant application or cloud audit logs. I would preserve evidence according to the organization's incident process and access controls. Investigation logs must identify events and affected resources without copying secret values, session tokens, authorization headers, or sensitive personal data.

I would then determine how the package could execute and what it could access. I would review whether it was a Composer plugin, whether the root project invoked any package binaries or callbacks, whether its classes or autoloaded files ran during requests or jobs, and whether CI or deployment steps executed its commands. I would map the operating-system identity, filesystem permissions, environment variables, mounted secrets, database permissions, cloud identity, network access, and external services available in each execution context.

Secret rotation would be based on credible reachability, not guesswork alone. If compromised code could read a secret or use an attached identity, I would treat that credential as potentially exposed even when exfiltration is not yet proven. I would prioritize externally usable and highly privileged credentials, including CI tokens, package-repository credentials, cloud keys, signing keys, database credentials, API tokens, session-signing keys, and deployment credentials. Where supported, I would revoke or disable the old credential first, issue a replacement, update dependent services in a controlled order, and verify that the old credential no longer works. For credentials that cannot be rotated without disruption, I would use a documented staged rotation or temporary access restriction.

I would also consider active sessions and derived credentials. Rotating a session-signing key may invalidate all sessions, which can be appropriate when the key was reachable but has a user-impact tradeoff. Rotating a database password does not by itself remove database persistence that an attacker may already have created. Therefore, credential rotation must be combined with access-log review, privilege review, persistence checks, and monitoring.

For remediation, I would prefer a fixed version or recovery procedure verified by trusted maintainers and reviewed internally. If no trusted fix exists, I would remove the package, disable the feature, replace it with a maintained alternative, pin an independently verified safe version, or create a minimal internal fork containing only reviewed changes. A rollback is safe only when the selected version and its distribution artifact are outside the affected scope. Version numbers alone must not be trusted when the incident involved altered tags, commits, repository access, or replaced archives.

I would make the smallest dependency change that resolves the incident while allowing required transitive updates. For example, I could perform a targeted package update with an explicit version and appropriate dependency flags rather than updating the entire dependency graph. I would inspect the complete composer.lock diff, including added, removed, upgraded, downgraded, and transitive packages. I would also review changes to Composer plugins, autoload configuration, package binaries, repositories, and root scripts.

I would rebuild the application on a clean, isolated CI runner or build host using a trusted Composer executable, trusted repositories, the reviewed composer.json, and the approved lockfile. Composer should not run as a privileged host user. During investigation, I would use --no-plugins --no-scripts when possible and perform the build inside a sandbox with restricted credentials and network access. This prevents Composer plugins and root-package scripts from executing during that step. If the application legitimately requires a plugin or root script, I would review it explicitly and enable only the minimum trusted functionality needed for the final build.

I would not reuse a possibly contaminated vendor directory, dependency cache, build workspace, CI runner, or container layer without validation. I would install dependencies from the reviewed lockfile and confirm that the resulting installed-package metadata matches it. Where the build system supports them, I would verify trusted checksums, source references, repository provenance, artifact attestations, or signatures. These controls help establish provenance but do not prove that trusted source code itself is harmless, so code review and behavioral testing remain necessary.

Testing would include unit, integration, regression, and security-focused tests for the affected paths. I would verify application startup, autoloading, dependency injection, web requests, background workers, scheduled jobs, command-line tools, authentication, authorization, session behavior, file handling, database access, and external integrations that the package could influence. I would test on supported PHP 8.4 and PHP 8.5 environments when those versions are part of the application's deployment matrix.

I would verify the remediation directly. I would confirm that no affected version, reference, archive, or package file remains in the release artifact; inspect installed-package metadata; rerun dependency audits; scan the artifact; and compare the final dependency graph with the approved lockfile. I would also verify that revoked credentials fail, replacement credentials work only where expected, outbound restrictions are effective, and the affected feature fails safely when its dependency or external service is unavailable.

Deployment would use the normal controlled release process with peer review, staged rollout, a canary when available, health checks, and a documented rollback or forward-fix plan. The rollback target must be independently verified as clean. If the incident may involve stolen credentials or persistence outside the application artifact, deploying corrected code alone is not sufficient; the related systems, identities, and data stores must also be remediated.

After deployment, I would monitor for published indicators of compromise and behavior related to the package's actual capabilities. Examples include unusual authentication attempts, unexpected credential use, new privileged accounts, suspicious outbound connections, process creation, modified files, unexplained scheduled jobs, abnormal database access, unexpected package downloads, integrity changes, and application error or latency changes. Monitoring should be time-bounded and risk-based, with alerts connected to an incident owner rather than merely collected.

Finally, I would document the timeline, affected assets, evidence, containment decisions, credential actions, remediation, verification results, residual risk, and lessons learned. Post-incident controls could include automated lockfile auditing, dependency inventories or software bills of materials, ownership for dependency alerts, repository allowlists, restricted Composer allow-plugins configuration, reviewed root scripts, least-privilege CI and runtime identities, isolated builds, short-lived credentials, network egress controls, immutable artifacts, provenance records, protected release workflows, faster patch procedures, and exercises for dependency incidents.

The main tradeoff is speed versus certainty. High-risk containment and credential revocation may be necessary before the investigation is complete. However, broad package updates, mass secret rotation, or emergency rollbacks can also cause outages and hide evidence. I would take immediate reversible actions first, then make permanent changes based on the dependency's confirmed versions, execution paths, reachable privileges, and available evidence.

Technical Approach
  1. Open a security incident, assign ownership, and pause unsafe deployments or updates.
  2. Confirm the affected package, versions, references, artifacts, time window, and trusted incident guidance.
  3. Inventory every repository, lockfile, artifact, image, build, host, and running environment containing the dependency.
  4. Classify each occurrence as direct, transitive, development-only, build-time, Composer-plugin, or runtime use.
  5. Contain the dependency according to its reachable capabilities while preserving relevant evidence.
  6. Analyze composer.json, composer.lock, the dependency tree, plugins, root scripts, autoloading, package binaries, runtime calls, and CI execution paths.
  7. Map the files, networks, data, services, identities, and secrets available to compromised code.
  8. Revoke and rotate credentials that were credibly reachable, prioritizing privileged and externally usable secrets.
  9. Patch, replace, remove, pin, or temporarily fork the package using independently reviewed and trusted inputs.
  10. Review the complete lockfile and dependency-graph changes.
  11. Rebuild from the approved lockfile in a clean, isolated environment without reusing suspect caches or workspaces.
  12. Run functional, regression, integration, and security verification, including direct confirmation that affected artifacts are absent.
  13. Deploy through a staged process with a verified clean rollback or forward-fix plan.
  14. Monitor relevant indicators and behavior, investigate possible persistence, and verify credential revocation.
  15. Document the incident and add preventive PHP and Composer supply-chain controls.
Practical Insights

The investigation time grows mainly with the number of repositories, lockfiles, artifacts, environments, dependency paths, credentials, and historical releases that must be checked. Reading one lockfile uses little time and memory, but organization-wide inventory searches, artifact scans, clean rebuilds, and regression tests may require substantial computing and engineering effort. A dependency tree or lockfile analysis generally uses memory proportional to the number of packages and relationships being processed; no exact bound should be claimed without knowing the Composer version and project graph. Secret rotation can create operational downtime when services depend on the old credentials. Long-term controls add build time, storage, and maintenance work, but they reduce future detection and recovery costs.

Why Interviewers Ask This

Interviewers want to know whether the candidate can manage a PHP software supply-chain incident rather than treating it as a routine package upgrade. The question evaluates dependency and lockfile analysis, containment judgment, evidence preservation, secret-rotation decisions, safe remediation, clean builds, deployment verification, monitoring, and practical post-incident prevention.

Common interview mistakes

Common mistakes include running an unrestricted composer update before recording the existing state; checking only composer.json instead of exact locked and deployed versions; ignoring packages-dev, transitive dependencies, historical releases, or Composer plugins; incorrectly claiming that scripts declared by ordinary dependencies are automatically executed; running investigation commands with untrusted installed plugins enabled; treating a clean composer audit result as proof of safety; trusting a version number without checking the affected reference or artifact; rolling back to an unverified release; rebuilding on a potentially contaminated runner or from a suspect cache; running Composer as root; enabling all plugins with an overly broad allow-plugins policy; rotating secrets without mapping access and service dependencies; failing to revoke old credentials; assuming rotation removes attacker persistence; logging secret values during investigation; claiming that data was stolen without evidence; testing only the application's happy path; and ending the response after patching without deployment verification, monitoring, or post-incident controls.

Interview tip

Present the response in incident order: confirm scope, inventory exact locked deployments, contain and preserve evidence, analyze execution and access, rotate reachable secrets, remediate, rebuild cleanly, verify, deploy safely, monitor, and prevent recurrence. Clearly distinguish confirmed compromise, possible exposure, and proven impact.

Interviewer may ask next
How would you determine whether the compromised dependency could have accessed application secrets?

I would identify every context in which its code could run, including Composer plugins, root-invoked package binaries, autoloaded files, application requests, workers, tests, and CI jobs. For each context, I would map the operating-system identity, environment variables, mounted secret files, filesystem permissions, cloud identity, database permissions, and network access. I would review logs and indicators for evidence of use, but absence of evidence would not prove non-exposure. Valuable credentials that were credibly reachable would be revoked and rotated in a controlled order.

What would you do if no trusted patched version or replacement package were available?

I would first disable or remove the affected functionality when the application can operate safely without it. Other options are rolling back to an independently verified artifact outside the affected scope, replacing the package with simpler internal code, or creating a minimal reviewed internal fork. I would isolate the temporary solution, restrict its permissions and network access, add focused tests and monitoring, document the accepted residual risk and owner, set an expiration date, and continue evaluating a maintained permanent replacement.

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.