DEV Community

Cover image for Secure PHP API
subhash periyasawmy
subhash periyasawmy

Posted on

Secure PHP API

demonstrating essential security measures like HTTPS enforcement, custom JWT stub, basic input validation, and a PDO-based prepared statement

  1. The Configuration and Utility Stubs (config.php and utils.php)

You’ll need a central place for configurations and a custom implementation for complex security mechanisms like JWT and database connection.

config.php
<?php
// Configuration (KEEP THIS FILE OUTSIDE THE WEB ROOT!)
define(‘DB_HOST’, ‘localhost’);
define(‘DB_NAME’, ‘api_db’);
define(‘DB_USER’, ‘api_reader’); // Use a dedicated, low-privilege user
define(‘DB_PASS’, ‘secure_db_password’);
define(‘JWT_SECRET’, ‘YOUR_VERY_LONG_AND_SECURE_SECRET_KEY’);
define(‘RATE_LIMIT_MAX’, 100);
define(‘RATE_LIMIT_TIME’, 60); // seconds
?>
Enter fullscreen mode Exit fullscreen mode
utils.php

    <?php
    require_once ‘config.php’;

    // — — Database Connection (PDO is Core PHP) — -
    function get_db_connection() {
    try {
    $pdo = new PDO(“mysql:host=” . DB_HOST . “;dbname=” . DB_NAME . “;charset=utf8mb4”, DB_USER, DB_PASS);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
    return $pdo;
    } catch (PDOException $e) {
    // Log the error internally and return a generic error to the client
    http_response_code(500);
    echo json_encode([“error” => “Database connection failed.”]);
    exit();
    }
    }

    // — — JWT Stub (Manual, Simplified Implementation) — -
    function validate_jwt() {
    $auth_header = $_SERVER[‘HTTP_AUTHORIZATION’] ?? ‘’;
    if (!preg_match(‘/Bearer\s(\S+)/’, $auth_header, $matches)) {
    return false;
    }
    $jwt = $matches[1];

    // In Core PHP without libraries, JWT validation is complex.
    // This is a minimal, NON-SECURE stub for demonstration.
    // In production, ALWAYS use a vetted library (like firebase/php-jwt).
    $parts = explode(‘.’, $jwt);
    if (count($parts) !== 3) {
    return false;
    }

    list($header_b64, $payload_b64, $signature_b64) = $parts;

    // In a real scenario, you MUST verify the signature here.
    // e.g., hash_hmac(‘sha256’, “$header_b64.$payload_b64”, JWT_SECRET, true)

    $payload_json = base64_decode($payload_b64);
    $payload = json_decode($payload_json, true);

    // Basic validity checks
    if (!isset($payload[‘exp’]) || $payload[‘exp’] < time()) {
    return false; // Token expired
    }
    if (!isset($payload[‘user_id’])) {
    return false; // Missing necessary claim
    }

    return $payload;
    }

    // — — Output Function for consistent responses — -
    function send_response($status_code, $data) {
    http_response_code($status_code);
    echo json_encode($data);
    exit();
    }
    ?>
Enter fullscreen mode Exit fullscreen mode
  1. The Main API Endpoint (api.php)

This file contains the logic for routing, authentication, and security checks.

<?php
require_once ‘utils.php’;

header(‘Content-Type: application/json’);

// — — 1. Essential Security Checks — -

// A. Enforce HTTPS
if (empty($_SERVER[‘HTTPS’]) || $_SERVER[‘HTTPS’] === ‘off’) {
send_response(403, [“error” => “API access requires HTTPS.”]);
}

// B. Rate Limiting (Using files, since no Redis/Memcached is assumed)
$ip = $_SERVER[‘REMOTE_ADDR’];
$log_file = ‘rate_limit_logs/’ . md5($ip) . ‘.log’;
$current_time = time();

// Clean up old requests and count current ones
if (file_exists($log_file)) {
$requests = file($log_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$valid_requests = array_filter($requests, function($timestamp) use ($current_time) {
return ($current_time — $timestamp) < RATE_LIMIT_TIME;
});

if (count($valid_requests) >= RATE_LIMIT_MAX) {
send_response(429, [“error” => “Rate limit exceeded.”]);
}

// Log new request and save valid ones
$valid_requests[] = $current_time;
file_put_contents($log_file, implode(“\n”, $valid_requests));
} else {
// New IP, create log file
if (!is_dir(‘rate_limit_logs’)) mkdir(‘rate_limit_logs’);
file_put_contents($log_file, $current_time);
}

// — — 2. Routing and Authorization — -

$method = $_SERVER[‘REQUEST_METHOD’];
// Use PATH_INFO for clean URLs like /api.php/users/123
$path_info = $_SERVER[‘PATH_INFO’] ?? ‘/’;
$uri_segments = array_values(array_filter(explode(‘/’, $path_info)));

$endpoint = $uri_segments[0] ?? ‘info’;
$resource_id = $uri_segments[1] ?? null;

// Endpoints requiring authentication
$protected_endpoints = [‘users’, ‘products’, ‘orders’];
if (in_array($endpoint, $protected_endpoints)) {
$auth_data = validate_jwt();
if (!$auth_data) {
send_response(401, [“error” => “Unauthorized: Invalid or missing token.”]);
}
$user_id = $auth_data[‘user_id’];
}

// — — 3. Endpoint Logic with Input Security — -

switch ($endpoint) {
case ‘users’:
if ($method === ‘GET’ && $resource_id) {
// C. Broken Object Level Authorization (BOLA) Check
if ((int)$resource_id !== $user_id) {
send_response(403, [“error” => “Forbidden: You can only view your own resource.”]);
}
handle_get_user((int)$resource_id);
} else {
send_response(405, [“error” => “Method not allowed for this endpoint.”]);
}
break;

case ‘login’:
if ($method === ‘POST’) {
handle_login();
}
break;
// … other endpoints …

default:
send_response(404, [“error” => “Endpoint not found.”]);
}

// — — 4. Controller Functions — -

function handle_get_user($id) {
$pdo = get_db_connection();

// D. Use Prepared Statements (Crucial for SQL Injection prevention)
$stmt = $pdo->prepare(“SELECT id, username, email FROM users WHERE id = :id”);
$stmt->bindParam(‘:id’, $id, PDO::PARAM_INT);

if ($stmt->execute() && $user = $stmt->fetch()) {
// E. Limit Data Exposure (Never return password hash!)
send_response(200, $user);
} else {
send_response(404, [“error” => “User not found.”]);
}
}

function handle_login() {
$data = json_decode(file_get_contents(‘php://input’), true);

// F. Input Validation and Sanitization
$username = $data[‘username’] ?? ‘’;
$password = $data[‘password’] ?? ‘’;

if (empty($username) || empty($password)) {
send_response(400, [“error” => “Missing username or password.”]);
}

$pdo = get_db_connection();
$stmt = $pdo->prepare(“SELECT id, password FROM users WHERE username = :username”);
$stmt->bindParam(‘:username’, $username);

if ($stmt->execute() && $user = $stmt->fetch()) {
// G. Secure Password Verification
if (password_verify($password, $user[‘password’])) {
// H. Issue a new token (STUB)
$new_jwt = ‘HEADER.PAYLOAD_USER_’ . $user[‘id’] . ‘.SIGNATURE’;
send_response(200, [“token” => $new_jwt, “message” => “Login successful”]);
}
}

// Generic failure message to prevent enumeration attacks
send_response(401, [“error” => “Invalid credentials.”]);
}
?>
Enter fullscreen mode Exit fullscreen mode

Top comments (0)