demonstrating essential security measures like HTTPS enforcement, custom JWT stub, basic input validation, and a PDO-based prepared statement
- 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
?>
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();
}
?>
- 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.”]);
}
?>
Top comments (0)