This document provides a comprehensive checklist for secure PHP development using php-aegis. Follow these guidelines to ensure your application follows security best practices.
- OWASP Top 10 Mapping
- PHP Configuration
- Input Validation
- Output Sanitization
- HTTP Security Headers
- Authentication & Sessions
- Database Security
- File Operations
- Cryptography
- Error Handling
- CI/CD Security
- Dependency Management
This section maps php-aegis features and checklist items to the OWASP Top 10 2021 vulnerabilities.
| OWASP ID | Vulnerability | php-aegis Coverage | Section |
|---|---|---|---|
| A01:2021 | Broken Access Control | Partial (Headers) | HTTP Headers |
| A02:2021 | Cryptographic Failures | Guidelines | Cryptography |
| A03:2021 | Injection | Full (Validator, Sanitizer, TurtleEscaper) | Input, Output |
| A04:2021 | Insecure Design | Guidelines | All Sections |
| A05:2021 | Security Misconfiguration | Full (Headers) | HTTP Headers, PHP Config |
| A06:2021 | Vulnerable Components | Guidelines | Dependencies |
| A07:2021 | Auth Failures | Guidelines | Authentication |
| A08:2021 | Data Integrity Failures | Partial (CSP) | HTTP Headers |
| A09:2021 | Logging Failures | Guidelines | Error Handling |
| A10:2021 | SSRF | Partial (Validator) | Input Validation |
Risk: Attackers access unauthorized resources or perform actions outside their permissions.
php-aegis Mitigations:
| Control | php-aegis Feature | Code Example |
|---|---|---|
| CSRF Prevention | Headers::secure() sets SameSite cookies |
Headers::secure() |
| Clickjacking | Headers::frameOptions('DENY') |
Headers::frameOptions() |
| CORS Policies | Headers::crossOrigin*Policy() |
Headers::crossOriginResourcePolicy() |
Checklist:
- Use
Headers::frameOptions('DENY')to prevent clickjacking - Implement proper session management (see Authentication)
- Validate user permissions on every request
- Use CSRF tokens for state-changing operations
- Apply principle of least privilege
Risk: Sensitive data exposed due to weak/missing encryption.
php-aegis Mitigations:
| Control | php-aegis Feature | Code Example |
|---|---|---|
| HTTPS Enforcement | Validator::httpsUrl() |
Validator::httpsUrl($url) |
| HSTS | Headers::strictTransportSecurity() |
Headers::strictTransportSecurity(31536000, true, true) |
Checklist:
- Use
Validator::httpsUrl()to reject non-HTTPS URLs - Enable HSTS with
Headers::strictTransportSecurity() - Never use MD5/SHA1 for security (see Cryptography)
- Use
random_bytes()for secure random data - Use Argon2id for password hashing
CI Enforcement:
# In php-lint.yml - checks for weak cryptography
- name: Check weak cryptography
run: grep -rEn 'md5\s*\(|sha1\s*\(' --include="*.php" src/Risk: Untrusted data interpreted as commands (SQL, XSS, OS, LDAP, Turtle).
php-aegis Mitigations:
| Attack Type | php-aegis Feature | Code Example |
|---|---|---|
| XSS (HTML) | Sanitizer::html() |
echo Sanitizer::html($input) |
| XSS (Attr) | Sanitizer::attr() |
value="<?= Sanitizer::attr($v) ?>" |
| XSS (JS) | Sanitizer::js() |
var x = <?= Sanitizer::js($v) ?> |
| Path Traversal | Validator::safeFilename() |
Validator::safeFilename($name) |
| Null Byte | Validator::noNullBytes() |
Validator::noNullBytes($path) |
| RDF/SPARQL | TurtleEscaper::string() |
TurtleEscaper::literal($v) |
| URL Injection | Sanitizer::url() |
href="<?= Sanitizer::url($u) ?>" |
| JSON Injection | Sanitizer::json() |
Sanitizer::json($data) |
Checklist:
- Use
Sanitizer::html()for all HTML output - Use
Sanitizer::attr()for HTML attributes - Use
Sanitizer::js()for inline JavaScript - Use
Sanitizer::json()for JSON responses - Use
TurtleEscaper::literal()for RDF/Turtle data - Use
Validator::safeFilename()for file operations - Use prepared statements for ALL database queries
CI Enforcement:
# In php-lint.yml - checks for injection patterns
- name: Check dangerous functions
run: |
grep -rEn 'eval\s*\(|exec\s*\(' --include="*.php" src/
grep -rEn 'echo\s+\$_(GET|POST)' --include="*.php" src/Risk: Missing or ineffective security controls in application design.
php-aegis Mitigations:
| Control | php-aegis Feature | Purpose |
|---|---|---|
| Secure Defaults | Headers::secure() |
One-call security setup |
| Type Safety | All methods require string types |
Prevents type confusion |
| Fail Secure | Validators return false on invalid input |
Reject by default |
Checklist:
- Call
Headers::secure()early in every request - Use
declare(strict_types=1)in all PHP files - Validate before processing, sanitize before output
- Reject invalid input (don't try to "fix" it)
- Design with defense in depth
Risk: Missing security hardening, default credentials, verbose errors.
php-aegis Mitigations:
| Misconfiguration | php-aegis Feature | Code Example |
|---|---|---|
| Missing CSP | Headers::contentSecurityPolicy() |
Headers::secure() |
| Missing HSTS | Headers::strictTransportSecurity() |
Headers::secure() |
| Server Leakage | Headers::removeInsecureHeaders() |
Headers::secure() |
| MIME Sniffing | Headers::contentTypeOptions() |
Headers::secure() |
| Missing Permissions-Policy | Headers::permissionsPolicy() |
Headers::secure() |
Headers set by Headers::secure():
Content-Security-Policy: default-src 'self'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=()
Checklist:
- Call
Headers::secure()on every response - Configure PHP securely (see PHP Configuration)
- Disable
display_errorsin production - Remove default credentials and accounts
- Review all security headers with securityheaders.com
Risk: Using libraries with known vulnerabilities.
php-aegis Design:
- Zero runtime dependencies - Only PHP 8.1+ built-ins
- No vulnerable dependencies to track in production
Checklist:
- Run
composer auditon every CI build - Keep PHP version updated (8.1+ required)
- Review dev dependencies before adding
- Enable Dependabot/Renovate for automatic updates
CI Enforcement:
# In php-lint.yml
- name: Run Composer audit
run: composer audit --format=plainRisk: Weak passwords, session hijacking, credential stuffing.
php-aegis Mitigations:
| Control | php-aegis Feature | Purpose |
|---|---|---|
| Session Security | Headers::secure() sets cookie flags |
SameSite, Secure |
Checklist:
- Use
password_hash()withPASSWORD_ARGON2ID - Use
password_verify()for constant-time comparison - Regenerate session ID on login (
session_regenerate_id(true)) - Set session cookie flags: HttpOnly, Secure, SameSite=Strict
- Implement rate limiting for authentication
- Use MFA for sensitive operations
Risk: Untrusted code execution, insecure CI/CD, missing integrity checks.
php-aegis Mitigations:
| Control | php-aegis Feature | Purpose |
|---|---|---|
| CSP | Headers::contentSecurityPolicy() |
Prevents inline script injection |
| SRI Support | Design for external script verification | Subresource Integrity |
Checklist:
- Use Content-Security-Policy to block inline scripts
- Pin GitHub Actions to commit SHAs (not tags)
- Verify
composer.lockin CI builds - Sign commits with GPG
- Use Subresource Integrity for CDN resources
CI Enforcement:
# Pin actions to SHA for integrity
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2Risk: Insufficient logging, missing alerting, undetected breaches.
Checklist:
- Log authentication attempts (success and failure)
- Log access control failures
- Log input validation failures (potential attacks)
- Don't log sensitive data (passwords, tokens, PII)
- Set up alerting for anomalous patterns
- Monitor error logs for security issues
Error Handler Pattern:
set_exception_handler(function (Throwable $e): void {
// Log for operators
error_log(sprintf('[%s] %s', get_class($e), $e->getMessage()));
// Generic response to users
http_response_code(500);
echo json_encode(['error' => 'An unexpected error occurred']);
exit(1);
});Risk: Attacker forces server to make requests to unintended destinations.
php-aegis Mitigations:
| Control | php-aegis Feature | Code Example |
|---|---|---|
| URL Validation | Validator::url() |
Validator::url($url) |
| HTTPS Enforcement | Validator::httpsUrl() |
Validator::httpsUrl($url) |
| Hostname Validation | Validator::hostname() |
Validator::hostname($host) |
| IP Validation | Validator::ip(), ipv4(), ipv6() |
Validator::ip($ip) |
Checklist:
- Validate all user-supplied URLs with
Validator::url() - Prefer
Validator::httpsUrl()to enforce HTTPS - Maintain allowlist of permitted domains/IPs
- Block requests to internal/private IP ranges
- Don't follow redirects blindly
Safe URL Fetching:
use PhpAegis\Validator;
function safeFetch(string $url): string {
// Validate URL format
if (!Validator::httpsUrl($url)) {
throw new InvalidArgumentException('Invalid or non-HTTPS URL');
}
// Parse and validate hostname
$host = parse_url($url, PHP_URL_HOST);
if (!$host || !Validator::domain($host)) {
throw new InvalidArgumentException('Invalid hostname');
}
// Block internal/private IPs (allowlist approach is better)
$ip = gethostbyname($host);
if (filter_var($ip, FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
throw new InvalidArgumentException('Private/reserved IP not allowed');
}
// Now safe to fetch
return file_get_contents($url);
}| php-aegis Class | OWASP Categories Addressed |
|---|---|
Validator |
A03, A10 |
Sanitizer |
A03 |
Headers |
A01, A02, A04, A05, A08 |
TurtleEscaper |
A03 |
Legend:
- Full Coverage: php-aegis provides direct protection
- Partial Coverage: php-aegis helps but additional measures needed
- Guidelines: Documentation and checklists provided
; Strict error reporting (development)
error_reporting = E_ALL
display_errors = Off
log_errors = On
; Session security
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Strict
session.use_strict_mode = 1
session.use_only_cookies = 1
; Disable dangerous functions
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_multi_exec,parse_ini_file,show_source,eval
; File upload limits
upload_max_filesize = 10M
max_file_uploads = 5
; Exposure reduction
expose_php = Off-
declare(strict_types=1)at top of every PHP file - Error display disabled in production (
display_errors = Off) - Error logging enabled (
log_errors = On) - Dangerous functions disabled where not needed
- PHP version exposure disabled (
expose_php = Off) - Session cookies are HttpOnly and Secure
- Appropriate memory and execution limits set
use PhpAegis\Validator;
// Always validate before use
$email = Validator::email($_POST['email'] ?? '') ? $_POST['email'] : null;
$url = Validator::httpsUrl($_POST['website'] ?? '') ? $_POST['website'] : null;
$id = Validator::uuid($_GET['id'] ?? '') ? $_GET['id'] : null;- Never trust user input - validate ALL external data
- Use
Validator::email()for email addresses - Use
Validator::httpsUrl()for URLs (enforce HTTPS) - Use
Validator::uuid()for identifiers - Use
Validator::int()with min/max bounds for integers - Use
Validator::noNullBytes()to prevent null byte injection - Use
Validator::safeFilename()for user-provided filenames - Use
Validator::printable()for text that should have no control chars - Reject invalid input rather than attempting to "fix" it
- Validate data types, lengths, formats, and ranges
- Use allowlists over denylists where possible
| Input Source | Risk Level | Required Validation |
|---|---|---|
$_GET |
High | Always validate |
$_POST |
High | Always validate |
$_FILES |
Critical | Validate + scan |
$_COOKIE |
High | Always validate |
$_SERVER |
Medium | Validate if user-influenced |
| Database | Medium | Validate on retrieval |
| APIs | Medium | Validate responses |
use PhpAegis\Sanitizer;
// HTML context
echo Sanitizer::html($userInput);
// HTML attribute context
echo '<input value="' . Sanitizer::attr($value) . '">';
// JavaScript context
echo '<script>var data = ' . Sanitizer::js($data) . ';</script>';
// URL context
echo '<a href="' . Sanitizer::url($link) . '">Link</a>';
// CSS context (limited support - prefer external stylesheets)
echo '<div style="color: ' . Sanitizer::css($color) . ';">';- Context-aware escaping - use the right method for each context
- Use
Sanitizer::html()for HTML body content - Use
Sanitizer::attr()for HTML attributes - Use
Sanitizer::js()for inline JavaScript - Use
Sanitizer::url()for URL components - Use
Sanitizer::json()for JSON output - Use
Sanitizer::filename()before file operations - Never use
htmlspecialchars()alone - it's not context-aware - Never output user data in
<script>withoutSanitizer::js() - Never use user data in CSS without validation
| Context | Safe Method | Unsafe |
|---|---|---|
| HTML body | Sanitizer::html() |
echo $var |
| HTML attribute | Sanitizer::attr() |
value="$var" |
| JavaScript | Sanitizer::js() |
var x = '$var' |
| URL | Sanitizer::url() |
href="$var" |
| CSS | Sanitizer::css() |
style="$var" |
| JSON | Sanitizer::json() |
json_encode() alone |
use PhpAegis\Headers;
// Apply all recommended security headers at once
Headers::secure();
// Or configure individually
Headers::contentSecurityPolicy("default-src 'self'; script-src 'self'");
Headers::strictTransportSecurity(31536000, true, true);
Headers::frameOptions('DENY');
Headers::contentTypeOptions();
Headers::referrerPolicy('strict-origin-when-cross-origin');
Headers::permissionsPolicy([
'camera' => [],
'microphone' => [],
'geolocation' => ['self'],
]);- Call
Headers::secure()early in request lifecycle - Use Content-Security-Policy (CSP) to prevent XSS
- Enable HSTS with
includeSubDomainsandpreload - Set
X-Frame-Options: DENYunless framing is needed - Set
X-Content-Type-Options: nosniff - Configure appropriate
Referrer-Policy - Restrict features with
Permissions-Policy - Remove server identification headers
| Header | Recommended Value |
|---|---|
| Content-Security-Policy | default-src 'self'; script-src 'self'; style-src 'self' |
| Strict-Transport-Security | max-age=31536000; includeSubDomains; preload |
| X-Frame-Options | DENY |
| X-Content-Type-Options | nosniff |
| Referrer-Policy | strict-origin-when-cross-origin |
| Permissions-Policy | camera=(), microphone=(), geolocation=() |
- Use
password_hash()withPASSWORD_ARGON2ID(orPASSWORD_BCRYPT) - Use
password_verify()for comparison (timing-safe) - Regenerate session ID on privilege change (
session_regenerate_id(true)) - Set session cookie flags:
HttpOnly,Secure,SameSite=Strict - Implement session timeout (idle and absolute)
- Use CSRF tokens for state-changing operations
- Rate-limit authentication attempts
- Log authentication events
- Use secure password reset flows (time-limited tokens)
// Minimum secure password hashing
$hash = password_hash($password, PASSWORD_ARGON2ID, [
'memory_cost' => 65536, // 64MB
'time_cost' => 4, // 4 iterations
'threads' => 3, // 3 parallel threads
]);
// Verify
if (password_verify($input, $hash)) {
// Check if rehash needed (algorithm updates)
if (password_needs_rehash($hash, PASSWORD_ARGON2ID)) {
$newHash = password_hash($input, PASSWORD_ARGON2ID);
// Update stored hash
}
}- Always use prepared statements with bound parameters
- Use PDO with
ATTR_EMULATE_PREPARES = false - Set
ATTR_ERRMODE = ERRMODE_EXCEPTION - Use least-privilege database accounts
- Escape identifiers (table/column names) if dynamic
- Validate and allowlist ORDER BY columns
- Limit query results appropriately
- Log slow queries and failures
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false, // Use real prepared statements
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::MYSQL_ATTR_MULTI_STATEMENTS => false, // Prevent multi-query attacks
]);
// Always use prepared statements
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]); // $id is bound, not interpolated- Validate file extensions against allowlist
- Validate MIME types (don't trust Content-Type header alone)
- Use
Validator::safeFilename()for user-provided names - Store uploads outside web root
- Generate random filenames for stored files
- Set restrictive permissions (0644 for files, 0755 for dirs)
- Limit file sizes at PHP and web server level
- Scan uploads for malware if handling untrusted files
- Never use user input in
include/requirepaths
use PhpAegis\Validator;
use PhpAegis\Sanitizer;
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'pdf'];
$maxSize = 5 * 1024 * 1024; // 5MB
$file = $_FILES['upload'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
throw new Exception('Upload failed');
}
if ($file['size'] > $maxSize) {
throw new Exception('File too large');
}
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowed, true)) {
throw new Exception('Invalid file type');
}
// Generate safe filename
$safeName = bin2hex(random_bytes(16)) . '.' . $ext;
$uploadDir = '/var/uploads/'; // Outside web root!
move_uploaded_file($file['tmp_name'], $uploadDir . $safeName);- Never use MD5 or SHA1 for security purposes
- Use
random_bytes()for secure random data - Use
hash_equals()for timing-safe comparisons - Use
sodium_*functions for encryption - Use
PASSWORD_ARGON2IDfor password hashing - Store secrets in environment variables, not code
- Use HTTPS everywhere (no HTTP)
- Pin certificates for sensitive API connections
| Use Case | Algorithm | PHP Function |
|---|---|---|
| Passwords | Argon2id | password_hash() |
| Random tokens | CSPRNG | random_bytes() |
| Data hashing | SHA-256+ | hash('sha256', ...) |
| HMAC | SHA-256+ | hash_hmac('sha256', ...) |
| Encryption | XChaCha20-Poly1305 | sodium_crypto_secretbox() |
| Key derivation | Argon2id | sodium_crypto_pwhash() |
| String comparison | Timing-safe | hash_equals() |
- Never expose stack traces to users in production
- Log errors with sufficient context for debugging
- Use custom error pages (don't reveal framework/PHP version)
- Handle exceptions at application boundaries
- Don't log sensitive data (passwords, tokens, PII)
- Monitor error logs for security issues
- Return generic error messages to clients
set_exception_handler(function (Throwable $e): void {
// Log full details
error_log(sprintf(
"[%s] %s in %s:%d\n%s",
get_class($e),
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString()
));
// Generic response to client
http_response_code(500);
echo json_encode(['error' => 'An unexpected error occurred']);
exit(1);
});- PHPStan at level 9 (maximum strictness)
- PHP-CS-Fixer with PSR-12 standard
- PHPUnit tests with coverage requirements
- Secret scanning (TruffleHog, git-secrets)
- Dependency vulnerability scanning (Composer audit)
- SAST scanning (CodeQL, Psalm)
- License compliance checking
- Pin GitHub Actions to commit SHAs (not version tags)
- Use minimal permissions (
permissions: read-all) - Never store secrets in code or logs
- Require code review for main branch
- Enable branch protection rules
- Sign commits with GPG
# Minimum CI checks for PHP projects
- PHPStan level 9
- PHP-CS-Fixer --dry-run
- PHPUnit with coverage
- composer audit (dependency vulnerabilities)
- TruffleHog (secret detection)
- SPDX license header check- Run
composer auditregularly - Use
composer.lockfor reproducible builds - Review dependencies before adding
- Minimize dependency count
- Prefer well-maintained packages
- Check licenses are compatible (SPDX)
- Enable Dependabot/Renovate for updates
- Verify package integrity (checksums)
{
"config": {
"sort-packages": true,
"preferred-install": "dist",
"optimize-autoloader": true,
"secure-http": true
},
"minimum-stability": "stable",
"prefer-stable": true
}- Validate all input with php-aegis Validator
- Escape all output with php-aegis Sanitizer
- Use prepared statements for all database queries
- Apply security headers with php-aegis Headers
- Use
declare(strict_types=1)everywhere - Use Argon2id for password hashing
- Pin CI action versions to commit SHAs
- Log security events
- Trust user input without validation
- Output user data without escaping
- Concatenate SQL queries with user input
- Use MD5/SHA1 for security
- Store secrets in code
- Expose error details to users
- Use
eval(),exec(), or similar functions - Disable HTTPS requirements
Run these commands to verify your configuration:
# Static analysis (max strictness)
composer analyze # PHPStan level 9
# Code style
composer lint # PHP-CS-Fixer check
# Tests
composer test # PHPUnit
# Dependency audit
composer audit # Check for vulnerabilities