Skip to content

Security: RailwayGunDora/fastway

Security

SECURITY.md

Security Considerations - Complete Guide

7. API Key Protection

Current Implementation ✅

Method 1: Environment Variables

// api/config.php
define('FASTWAY_API_KEY', getenv('FASTWAY_API_KEY') ?: 'fallback_key');

Method 2: Server-Side Only

  • API key NEVER sent to client
  • All API calls made from server-side PHP
  • JavaScript cannot access the key

Method 3: File Protection (.htaccess)

# Deny direct access to config files
<Files "config.php">
    Require all denied
</Files>

<FilesMatch "\.env$">
    Require all denied
</FilesMatch>

Method 4: Google App Engine (app.yaml)

env_variables:
  FASTWAY_API_KEY: 'your_secure_key_here'

Method 5: Google Secret Manager (Production)

use Google\Cloud\SecretManager\V1\SecretManagerServiceClient;

$client = new SecretManagerServiceClient();
$name = 'projects/PROJECT_ID/secrets/fastway-api-key/versions/latest';
$response = $client->accessSecretVersion($name);
$apiKey = $response->getPayload()->getData();

Protection Checklist ✅

  • Not in version control (.gitignore)
  • Not in client-side code
  • Environment variable based
  • File access restrictions
  • Secret management for production

8. Input Validation & Sanitization

Implementation

// api/config.php
function sanitizeInput($input, $type = 'general') {
    if (empty($input)) {
        return '';
    }
    
    // Basic sanitization
    $input = trim($input);
    $input = stripslashes($input);
    $input = htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
    
    // Type-specific sanitization
    switch($type) {
        case 'tracking':
            // Only alphanumeric
            return preg_replace('/[^a-zA-Z0-9]/', '', $input);
            
        case 'postal':
            // Only digits, max 4
            $postal = preg_replace('/[^0-9]/', '', $input);
            return substr($postal, 0, 4);
            
        case 'numeric':
            // Float validation
            return filter_var($input, FILTER_SANITIZE_NUMBER_FLOAT, 
                              FILTER_FLAG_ALLOW_FRACTION);
            
        case 'email':
            return filter_var($input, FILTER_SANITIZE_EMAIL);
            
        case 'suburb':
            // Letters, spaces, hyphens only
            return preg_replace('/[^a-zA-Z\s\-]/', '', $input);
            
        default:
            return $input;
    }
}

function validateInput($input, $type, $rules = []) {
    switch($type) {
        case 'tracking':
            // Must be 8-15 alphanumeric characters
            return preg_match('/^[A-Z0-9]{8,15}$/i', $input);
            
        case 'postal':
            // Exactly 4 digits
            return preg_match('/^[0-9]{4}$/', $input);
            
        case 'weight':
            // Between 0.1 and 30 kg
            $weight = floatval($input);
            return $weight >= 0.1 && $weight <= 30;
            
        case 'suburb':
            // 2-50 characters, letters and spaces
            return strlen($input) >= 2 && strlen($input) <= 50;
            
        default:
            return !empty($input);
    }
}

Usage in API Endpoints

// api/track.php
$trackingNumber = sanitizeInput($_POST['tracking_number'] ?? '', 'tracking');

if (!validateInput($trackingNumber, 'tracking')) {
    sendResponse(false, [], 'Invalid tracking number format');
    exit;
}

// api/quote.php
$suburb = sanitizeInput($_POST['suburb'] ?? '', 'suburb');
$postalCode = sanitizeInput($_POST['postal_code'] ?? '', 'postal');
$weight = sanitizeInput($_POST['weight'] ?? '', 'numeric');

if (!validateInput($suburb, 'suburb')) {
    sendResponse(false, [], 'Invalid suburb name');
    exit;
}

if (!validateInput($postalCode, 'postal')) {
    sendResponse(false, [], 'Invalid postal code');
    exit;
}

if (!validateInput($weight, 'weight')) {
    sendResponse(false, [], 'Weight must be between 0.1 and 30 kg');
    exit;
}

Multi-Layer Defense

Layer 1: Client-Side (UX)

// assets/js/track.js
if (!trackingNum || trackingNum.length < 3) {
    showError('Please enter a valid tracking number');
    return;
}

Layer 2: Server-Side Sanitization

$input = sanitizeInput($input, $type);

Layer 3: Server-Side Validation

if (!validateInput($input, $type)) {
    sendResponse(false, [], 'Invalid input');
}

Layer 4: Output Encoding

echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');

9. Potential Security Risks

1. API Key Exposure 🔴 Critical

Risk: Key leaked in client code
Impact: Unauthorized API usage, financial cost
Mitigation: ✅ Server-side only, environment variables

2. XSS (Cross-Site Scripting) 🔴 Critical

Risk: Malicious JavaScript injection
Impact: Session hijacking, data theft
Mitigation:

// Always escape output
echo htmlspecialchars($data, ENT_QUOTES, 'UTF-8');

// Content Security Policy
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net");

3. SQL Injection 🔴 Critical (if database used)

Risk: Malicious SQL in user input
Impact: Data breach, data loss
Mitigation:

// Use prepared statements
$stmt = $pdo->prepare("SELECT * FROM tracking WHERE number = :number");
$stmt->execute(['number' => $trackingNumber]);

4. CSRF (Cross-Site Request Forgery) 🟡 High

Risk: Unauthorized actions
Impact: Unwanted API calls
Mitigation:

// Generate token
session_start();
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));

// Validate token
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
    http_response_code(403);
    exit('CSRF validation failed');
}

5. API Abuse 🟡 High

Risk: DoS, scraping
Impact: High costs, service disruption
Mitigation: Rate limiting, CAPTCHA

6. Information Disclosure 🟡 High

Risk: Error messages reveal system details
Impact: Aids attackers
Mitigation:

// Production error handling
if (getenv('APP_ENV') === 'production') {
    ini_set('display_errors', 0);
    error_reporting(0);
}

// Generic error messages to users
try {
    // ... code
} catch (Exception $e) {
    // Log detailed error
    logError($e->getMessage());
    // Generic message to user
    sendResponse(false, [], 'An error occurred. Please try again.');
}

7. Man-in-the-Middle 🟡 High

Risk: Intercepted communications
Impact: Data theft
Mitigation:

// Force HTTPS
if (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') {
    header('Location: https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
    exit;
}

// HSTS Header
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');

8. Session Hijacking 🟢 Medium

Risk: Stolen session cookies
Impact: Unauthorized access
Mitigation:

// Secure session configuration
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_samesite', 'Strict');
session_start();

10. Prevention Strategies

A. Prevent API Abuse

Method 1: Rate Limiting

class RateLimiter {
    private $redis;
    
    public function check($identifier, $limit = 100, $window = 3600) {
        $key = "rate_limit:{$identifier}";
        $current = $this->redis->incr($key);
        
        if ($current === 1) {
            $this->redis->expire($key, $window);
        }
        
        if ($current > $limit) {
            http_response_code(429);
            echo json_encode(['error' => 'Too many requests']);
            exit;
        }
        
        return true;
    }
}

// Usage in api/track.php
$rateLimiter = new RateLimiter();
$clientIp = $_SERVER['REMOTE_ADDR'];
$rateLimiter->check($clientIp, 100, 3600); // 100 requests/hour

Method 2: Token Bucket Algorithm

class TokenBucket {
    private $capacity = 10;     // Max requests
    private $refillRate = 1;    // Tokens per second
    private $tokens;
    private $lastRefill;
    
    public function consume($count = 1) {
        $this->refill();
        
        if ($this->tokens >= $count) {
            $this->tokens -= $count;
            return true;
        }
        
        return false; // Rate limit exceeded
    }
    
    private function refill() {
        $now = time();
        $elapsed = $now - $this->lastRefill;
        $tokensToAdd = $elapsed * $this->refillRate;
        
        $this->tokens = min($this->capacity, $this->tokens + $tokensToAdd);
        $this->lastRefill = $now;
    }
}

Method 3: API Key-Based Limits

class APIKeyLimiter {
    public function checkLimit($apiKey) {
        $usage = $this->getUsage($apiKey);
        $plan = $this->getPlan($apiKey);
        
        if ($usage >= $plan['max_requests']) {
            http_response_code(429);
            echo json_encode(['error' => 'API quota exceeded']);
            exit;
        }
    }
}

B. Prevent Automated Spam

Method 1: Google reCAPTCHA

<!-- In form -->
<script src="https://www.google.com/recaptcha/api.js"></script>
<div class="g-recaptcha" data-sitekey="YOUR_SITE_KEY"></div>
// Verify in PHP
function verifyCaptcha($response) {
    $secret = getenv('RECAPTCHA_SECRET');
    $verify = file_get_contents(
        "https://www.google.com/recaptcha/api/siteverify?secret={$secret}&response={$response}"
    );
    $data = json_decode($verify);
    return $data->success;
}

if (!empty($_POST['g-recaptcha-response'])) {
    if (!verifyCaptcha($_POST['g-recaptcha-response'])) {
        sendResponse(false, [], 'CAPTCHA verification failed');
        exit;
    }
}

Method 2: Honeypot Field

<!-- Hidden field that bots will fill -->
<input type="text" name="website" style="display:none" tabindex="-1" autocomplete="off">
// Reject if honeypot is filled
if (!empty($_POST['website'])) {
    // Bot detected - reject silently
    exit;
}

Method 3: Timing Check

session_start();
$_SESSION['form_loaded_at'] = time();

// On submit
$timeTaken = time() - $_SESSION['form_loaded_at'];
if ($timeTaken < 2) {
    // Too fast - likely a bot
    exit;
}

Method 4: Request Fingerprinting

function generateFingerprint() {
    return hash('sha256', implode('|', [
        $_SERVER['REMOTE_ADDR'],
        $_SERVER['HTTP_USER_AGENT'],
        $_SERVER['HTTP_ACCEPT_LANGUAGE']
    ]));
}

// Track fingerprints
$fingerprint = generateFingerprint();
$requestCount = $redis->incr("fp:{$fingerprint}");

if ($requestCount > 10) {
    // Suspicious activity
    exit;
}

C. Prevent Injection Attacks

1. SQL Injection Prevention

// ✅ CORRECT - Prepared Statements
$stmt = $pdo->prepare("SELECT * FROM tracking WHERE number = :number");
$stmt->execute(['number' => $trackingNumber]);

// ❌ WRONG - String concatenation
$query = "SELECT * FROM tracking WHERE number = '{$trackingNumber}'";

2. XSS Prevention

// ✅ CORRECT - Always escape output
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');

// ❌ WRONG - Direct output
echo $userInput;

3. Command Injection Prevention

// ✅ CORRECT - Validate and whitelist
$allowedCommands = ['convert', 'resize'];
if (in_array($command, $allowedCommands)) {
    exec(escapeshellcmd($command));
}

// ❌ WRONG - Direct execution
exec($_POST['command']);

4. Path Traversal Prevention

// ✅ CORRECT - Validate path
$filename = basename($_GET['file']);
$filepath = __DIR__ . '/uploads/' . $filename;

if (realpath($filepath) !== $filepath) {
    die('Invalid file path');
}

// ❌ WRONG - Direct path
$file = __DIR__ . '/' . $_GET['file'];

5. LDAP Injection Prevention

// ✅ CORRECT - Escape special chars
function escapeLDAP($str) {
    $metaChars = ['\\', '*', '(', ')', "\x00"];
    $quotedMetaChars = [];
    
    foreach ($metaChars as $char) {
        $quotedMetaChars[] = '\\' . sprintf('%02x', ord($char));
    }
    
    return str_replace($metaChars, $quotedMetaChars, $str);
}

6. XML Injection Prevention

// ✅ CORRECT - Disable external entities
libxml_disable_entity_loader(true);
$xml = simplexml_load_string($data, 'SimpleXMLElement', LIBXML_NOENT);

11. POPIA/GDPR Compliance

Data Protection Implementation

1. Data Encryption

class EncryptionService {
    private $cipher = 'AES-256-CBC';
    private $key;
    
    public function __construct() {
        $this->key = getenv('ENCRYPTION_KEY');
    }
    
    public function encrypt($data) {
        $iv = openssl_random_pseudo_bytes(16);
        $encrypted = openssl_encrypt($data, $this->cipher, $this->key, 0, $iv);
        return base64_encode($iv . $encrypted);
    }
    
    public function decrypt($data) {
        $data = base64_decode($data);
        $iv = substr($data, 0, 16);
        $encrypted = substr($data, 16);
        return openssl_decrypt($encrypted, $this->cipher, $this->key, 0, $iv);
    }
}

2. Data Minimization

// Only collect necessary data
$tracking = [
    'tracking_number' => $trackingNumber,
    'status' => $status,
    'searched_at' => time()
    // Don't store: names, addresses, phone numbers unless required
];

3. Data Retention Policy

class DataRetentionService {
    public function cleanOldData() {
        // Delete data older than retention period (e.g., 12 months)
        $pdo->exec("
            DELETE FROM tracking_history 
            WHERE created_at < DATE_SUB(NOW(), INTERVAL 12 MONTH)
        ");
        
        // Log deletion
        $this->logDeletion('tracking_history', $pdo->rowCount());
    }
}

// Run via cron job
// 0 0 * * 0  php /path/to/cleanup.php

4. Consent Management

class ConsentManager {
    public function recordConsent($userId, $purpose, $consented) {
        $stmt = $pdo->prepare("
            INSERT INTO user_consents (user_id, purpose, consented, consented_at, ip_address)
            VALUES (?, ?, ?, NOW(), ?)
        ");
        $stmt->execute([
            $userId,
            $purpose,
            $consented ? 1 : 0,
            $_SERVER['REMOTE_ADDR']
        ]);
    }
    
    public function hasConsent($userId, $purpose) {
        $stmt = $pdo->prepare("
            SELECT consented FROM user_consents 
            WHERE user_id = ? AND purpose = ?
            ORDER BY consented_at DESC LIMIT 1
        ");
        $stmt->execute([$userId, $purpose]);
        $result = $stmt->fetch();
        return $result && $result['consented'] == 1;
    }
}

5. Right to be Forgotten

class DataDeletionService {
    public function deleteUserData($userId) {
        $pdo->beginTransaction();
        
        try {
            // Delete from all tables
            $tables = ['tracking_history', 'quotes', 'user_consents', 'users'];
            
            foreach ($tables as $table) {
                $pdo->prepare("DELETE FROM {$table} WHERE user_id = ?")
                    ->execute([$userId]);
            }
            
            // Log deletion for compliance
            $pdo->prepare("
                INSERT INTO deletion_log (user_id, deleted_at, reason, deleted_by)
                VALUES (?, NOW(), 'User request - GDPR', ?)
            ")->execute([$userId, getCurrentAdminId()]);
            
            $pdo->commit();
            
            return true;
        } catch (Exception $e) {
            $pdo->rollBack();
            throw $e;
        }
    }
}

6. Data Access Request

class DataAccessService {
    public function exportUserData($userId) {
        $data = [];
        
        // Get all user data
        $data['tracking_history'] = $this->getTrackingHistory($userId);
        $data['quote_history'] = $this->getQuoteHistory($userId);
        $data['consents'] = $this->getConsents($userId);
        
        // Generate JSON export
        $json = json_encode($data, JSON_PRETTY_PRINT);
        
        // Log access request
        $this->logAccessRequest($userId);
        
        return $json;
    }
}

7. Audit Logging

class AuditLogger {
    public function logAccess($userId, $action, $data) {
        $stmt = $pdo->prepare("
            INSERT INTO audit_log 
            (user_id, action, data, ip_address, user_agent, accessed_at)
            VALUES (?, ?, ?, ?, ?, NOW())
        ");
        
        $stmt->execute([
            $userId,
            $action,
            json_encode($data),
            $_SERVER['REMOTE_ADDR'],
            $_SERVER['HTTP_USER_AGENT']
        ]);
    }
}

// Log every data access
$auditLogger->logAccess($userId, 'tracking_search', ['number' => $trackingNumber]);

8. Privacy Policy & Cookie Consent

// Display privacy banner
if (!isset($_COOKIE['privacy_accepted'])) {
    // Show banner
}

// Privacy policy page
class PrivacyPolicy {
    public function getPolicy() {
        return [
            'data_collected' => ['tracking numbers', 'search history'],
            'purpose' => 'Provide tracking services',
            'retention' => '12 months',
            'rights' => ['access', 'rectification', 'erasure', 'portability'],
            'contact' => 'privacy@company.com'
        ];
    }
}

9. Data Breach Notification

class BreachNotificationService {
    public function notifyBreach($affectedUsers, $breachDetails) {
        // Notify within 72 hours (GDPR requirement)
        foreach ($affectedUsers as $user) {
            $this->sendBreachNotification($user, $breachDetails);
        }
        
        // Notify data protection authority
        $this->notifyDPA($breachDetails);
        
        // Log notification
        $this->logBreachNotification($breachDetails);
    }
}

POPIA/GDPR Compliance Checklist

  • Lawful basis for processing
  • Consent mechanism
  • Data minimization
  • Purpose limitation
  • Storage limitation (retention policy)
  • Security measures (encryption)
  • Data subject rights (access, erasure)
  • Breach notification procedures
  • Privacy by design
  • Data protection officer (if required)
  • Privacy policy published
  • Audit logging

Security Best Practices Summary

1. Input/Output

  • ✅ Sanitize ALL user input
  • ✅ Validate on both client and server
  • ✅ Escape ALL output
  • ✅ Use prepared statements for SQL

2. Authentication/Authorization

  • ✅ Use strong session management
  • ✅ Implement CSRF protection
  • ✅ Use secure cookies
  • ✅ Rate limit login attempts

3. API Security

  • ✅ Keep keys server-side only
  • ✅ Use environment variables
  • ✅ Implement rate limiting
  • ✅ Add request signing (optional)

4. Data Protection

  • ✅ Encrypt sensitive data
  • ✅ Use HTTPS only
  • ✅ Implement data retention
  • ✅ Enable audit logging

5. Error Handling

  • ✅ Never expose system details
  • ✅ Log errors securely
  • ✅ Use generic error messages
  • ✅ Monitor for anomalies

6. Headers

header('X-Frame-Options: DENY');
header('X-Content-Type-Options: nosniff');
header('X-XSS-Protection: 1; mode=block');
header('Strict-Transport-Security: max-age=31536000');
header('Content-Security-Policy: default-src \'self\'');
header('Referrer-Policy: no-referrer');

Security is a continuous process, not a one-time setup!

There aren't any published security advisories