- Minimum: PHP 7.4
- Recommended: PHP 8.2+ for active security updates and performance improvements
- Tested: PHP 7.4, 8.0, 8.1, 8.2, 8.3, 8.4
Note: PHP versions below 8.1 have reached end-of-life and no longer receive security updates from the PHP team. While this SDK supports PHP 7.4+, we strongly recommend using PHP 8.2 or later in production environments.
The FamilySearch PHP Lite SDK provides:
- Optional AES-256-GCM encryption for OAuth access tokens stored in PHP sessions
- Authenticated encryption with tamper detection (prevents ciphertext modification)
- Automatic key normalization supporting multiple key formats (raw, base64, hex, passphrase)
- Backward compatibility for seamless migration from plaintext to encrypted storage
- Fail-secure behavior (encryption failures never fall back to plaintext storage)
As a developer using this SDK, you are responsible for:
- Enabling encryption in production environments
- Generating and managing secure encryption keys
- Configuring PHP session settings securely
- Setting proper file permissions on session storage directories
- Enforcing HTTPS for all API communications
- Implementing secure session management practices
// Default configuration (NOT SECURE for production)
$fs = new FamilySearch([
'appKey' => 'your-app-key',
'sessionEncryption' => false // Default: tokens stored in plaintext
]);Risk: If an attacker gains read access to your server's filesystem, they can read session files and extract access tokens. This could happen through:
- Misconfigured file permissions
- Backup file exposure
- Server compromise
- Shared hosting environment vulnerabilities
- Container/VM snapshot leaks
✅ Recommended: Enable AES-256-GCM encryption for production:
$fs = new FamilySearch([
'appKey' => $_ENV['FS_APP_KEY'],
'sessionEncryption' => true,
'sessionEncryptionKey' => $_ENV['FS_SESSION_ENCRYPTION_KEY']
]);Encryption provides defense-in-depth against:
✅ Passive filesystem access (attacker reads session files from disk)
✅ Backup exposure (encrypted session files in backups remain protected)
✅ Forensic analysis (disk forensics cannot recover plaintext tokens)
✅ Accidental logging (encrypted values logged instead of plaintext tokens)
✅ Container/VM snapshots (session data remains encrypted in snapshots)
✅ Shared hosting risks (other tenants cannot read your tokens)
Encryption is not a silver bullet. It does NOT protect against:
❌ Memory dumps (tokens are plaintext in PHP process memory during execution)
❌ Active server compromise (attacker with code execution can access encryption keys)
❌ XSS attacks (client-side JavaScript attacks bypass server-side encryption)
❌ Session hijacking (valid session IDs grant access regardless of encryption)
❌ Stolen encryption keys (attacker with key can decrypt all tokens)
❌ Network interception (HTTPS is required separately for transport security)
Bottom Line: Encryption protects data at rest on disk. You still need proper access controls, secure coding practices, HTTPS, and secure session management.
✅ Correct: Use cryptographically secure random bytes
# Generate a secure 32-byte key and encode as base64
php -r "echo base64_encode(random_bytes(32));"
# Output: WdaFfj4iL3Epz2o9phaBbh7FyA5fJs3lCcr6YB4QQxo=
# Alternative: Using OpenSSL
php -r "echo base64_encode(openssl_random_pseudo_bytes(32));"
# Or generate hex format
php -r "echo bin2hex(random_bytes(32));"
# Output: 4c0bd859f72d55003baa72e76fea385e599c9562b1b75a1fec0831b19f04118a❌ Wrong: Weak or predictable keys
// DO NOT DO THIS - Weak keys
'sessionEncryptionKey' => 'mysecretkey' // Too short, predictable
'sessionEncryptionKey' => 'password123' // Dictionary word
'sessionEncryptionKey' => md5('my-app-name') // Predictable
'sessionEncryptionKey' => date('Y-m-d') // Guessable✅ Correct: Environment variables
// Load key from environment (12-factor app pattern)
$fs = new FamilySearch([
'sessionEncryptionKey' => $_ENV['FS_SESSION_ENCRYPTION_KEY']
]);# Set environment variable in production
export FS_SESSION_ENCRYPTION_KEY="WdaFfj4iL3Epz2o9phaBbh7FyA5fJs3lCcr6YB4QQxo="
# Or use .env file (excluded from version control)
echo "FS_SESSION_ENCRYPTION_KEY=WdaFfj4iL3Epz2o9phaBbh7FyA5fJs3lCcr6YB4QQxo=" >> .env❌ Wrong: Hardcoded in source code
// DO NOT DO THIS - Key in source code
$fs = new FamilySearch([
'sessionEncryptionKey' => 'WdaFfj4iL3Epz2o9phaBbh7FyA5fJs3lCcr6YB4QQxo=' // NEVER COMMIT THIS
]);For production environments, use dedicated secrets management:
Cloud Providers:
- AWS: AWS Secrets Manager or Parameter Store
- Azure: Azure Key Vault
- GCP: Google Secret Manager
- Heroku: Config Vars
- Docker: Docker Secrets
Self-Hosted:
- HashiCorp Vault
- Kubernetes Secrets
- Ansible Vault
Example with AWS Secrets Manager:
// Retrieve key from AWS Secrets Manager
$client = new SecretsManagerClient(['region' => 'us-east-1']);
$result = $client->getSecretValue(['SecretId' => 'fs-session-encryption-key']);
$key = json_decode($result['SecretString'], true)['key'];
$fs = new FamilySearch([
'sessionEncryptionKey' => $key
]);Rotate encryption keys periodically (every 90 days recommended):
Step 1: Generate new key
php -r "echo base64_encode(random_bytes(32));"Step 2: Deploy new key (keep old key available temporarily)
# Set new key
export FS_SESSION_ENCRYPTION_KEY_NEW="<new-key>"Step 3: Migrate sessions (users re-authenticate naturally over time)
- Old sessions decrypt with old key
- New sessions encrypt with new key
- After migration period (7-30 days), remove old key
Step 4: Update application
// Try new key first, fallback to old key during migration
$keys = [
$_ENV['FS_SESSION_ENCRYPTION_KEY_NEW'], // Primary key
$_ENV['FS_SESSION_ENCRYPTION_KEY_OLD'] // Fallback during migration
];✅ Use different keys per environment:
# Development
FS_SESSION_ENCRYPTION_KEY="dev-key-here"
# Staging
FS_SESSION_ENCRYPTION_KEY="staging-key-here"
# Production
FS_SESSION_ENCRYPTION_KEY="production-key-here"❌ Never reuse keys across environments. If a development key is compromised, it should not affect production.
Verify your session directory permissions:
# Find your session directory
php -r "echo session_save_path();"
# Check permissions
ls -ld /var/lib/php/sessions
# Should show: drwx------ (700) - only owner can read/write/execute✅ Secure configuration:
# Set proper permissions (owner only)
sudo chmod 700 /var/lib/php/sessions
sudo chown www-data:www-data /var/lib/php/sessions # Use your web server user❌ Insecure configurations to avoid:
# DO NOT DO THIS
sudo chmod 777 /var/lib/php/sessions # World-readable! Anyone can read tokens!
sudo chmod 755 /var/lib/php/sessions # World-readable!Edit /etc/php/8.x/apache2/php.ini (or /etc/php/8.x/fpm/php.ini):
; Session save path with proper permissions
session.save_path = "/var/lib/php/sessions"
; Use strict mode - reject uninitialized session IDs
session.use_strict_mode = 1
; Cookies only (no URL session IDs)
session.use_cookies = 1
session.use_only_cookies = 1
; HTTPS only in production (prevents interception)
session.cookie_secure = 1
; HTTP only (prevents JavaScript access - XSS mitigation)
session.cookie_httponly = 1
; SameSite protection (CSRF mitigation)
session.cookie_samesite = "Strict"
; Prevent session fixation
session.use_trans_sid = 0
; Regenerate session ID after authentication
; (implement in your application code)
; Strong session ID entropy
session.sid_length = 48
session.sid_bits_per_character = 6Apply configuration changes:
# Apache
sudo systemctl restart apache2
# Nginx + PHP-FPM
sudo systemctl restart php8.x-fpm
sudo systemctl restart nginxTest script to verify session configuration:
<?php
// test_session_security.php
session_start();
echo "Session Configuration:\n";
echo "=====================\n";
echo "session.use_strict_mode: " . ini_get('session.use_strict_mode') . "\n";
echo "session.cookie_secure: " . ini_get('session.cookie_secure') . "\n";
echo "session.cookie_httponly: " . ini_get('session.cookie_httponly') . "\n";
echo "session.cookie_samesite: " . ini_get('session.cookie_samesite') . "\n";
echo "session.use_trans_sid: " . ini_get('session.use_trans_sid') . "\n";
echo "session.save_path: " . session_save_path() . "\n";
// Check permissions
$path = session_save_path();
$perms = substr(sprintf('%o', fileperms($path)), -3);
echo "\nSession directory permissions: $perms\n";
if ($perms === '700') {
echo "✅ Permissions are secure\n";
} else {
echo "❌ WARNING: Permissions should be 700, current: $perms\n";
}
?>Apache .htaccess:
# Force HTTPS
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]Nginx:
# Force HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
# Strong SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
# ... rest of configuration
}Use this checklist before deploying to production:
- Encryption enabled (
sessionEncryption: true) - Encryption key generated using
random_bytes(32) - Encryption key stored in environment variable (not hardcoded)
- Different keys per environment (dev, staging, production)
- No credentials hardcoded in application code
- No credentials committed to version control
- Session directory permissions set to
700(owner only) - Session directory owner is web server user (e.g.,
www-data) - PHP session settings configured securely (see above)
-
session.cookie_secure = 1(HTTPS only) -
session.cookie_httponly = 1(no JavaScript access) -
session.cookie_samesite = "Strict"(CSRF protection) - HTTPS enforced for entire application
- Valid SSL certificate installed and auto-renewing
- Security headers configured (CSP, X-Frame-Options, etc.)
- Error logging enabled but errors not displayed to users
- Key rotation schedule established (every 90 days)
- Security updates process for PHP and dependencies
- Backup encryption keys stored securely (encrypted backups)
- Incident response plan documented
- Test encryption works correctly in staging
- Test session persistence across requests
- Test key rotation procedure
- Test HTTPS enforcement (HTTP should redirect)
- Verify session cookies have secure flags set
Risk Level: HIGH
Attack Scenario:
- Attacker gains read access to server filesystem
- Session files in
/var/lib/php/sessionsare readable - Attacker extracts plaintext OAuth tokens from session files
Mitigation:
-
Enable encryption (primary defense):
'sessionEncryption' => true, 'sessionEncryptionKey' => $_ENV['FS_SESSION_ENCRYPTION_KEY']
-
Set proper file permissions (defense-in-depth):
chmod 700 /var/lib/php/sessions
-
Use separate storage (advanced):
// Store sessions in Redis or Memcached session.save_handler = redis session.save_path = "tcp://127.0.0.1:6379"
Residual Risk: LOW (after mitigations)
Risk Level: HIGH
Attack Scenario:
- Attacker intercepts session cookie (via XSS, network sniffing, or malware)
- Attacker uses stolen session ID to impersonate user
- Encryption doesn't prevent this (attacker has valid session ID)
Mitigation:
-
Enforce HTTPS (prevent network interception):
session.cookie_secure = 1 -
HTTPOnly cookies (prevent XSS theft):
session.cookie_httponly = 1 -
SameSite cookies (prevent CSRF):
session.cookie_samesite = "Strict"
-
Regenerate session ID after login:
// After successful authentication session_regenerate_id(true);
-
IP/User-Agent validation (advanced):
$_SESSION['ip'] = $_SERVER['REMOTE_ADDR']; $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT']; // Verify on subsequent requests
Residual Risk: MEDIUM (after mitigations)
Risk Level: CRITICAL
Attack Scenario:
- Encryption key hardcoded in source code committed to Git
- Attacker accesses GitHub repository
- All encrypted sessions can be decrypted
Mitigation:
-
Never commit keys to version control:
# Add to .gitignore echo ".env" >> .gitignore echo "config/secrets.php" >> .gitignore
-
Use environment variables:
export FS_SESSION_ENCRYPTION_KEY="<key>"
-
Use secrets management (production):
- AWS Secrets Manager
- HashiCorp Vault
- Azure Key Vault
-
Scan for leaked secrets:
# Use git-secrets or similar git secrets --install git secrets --register-aws
Residual Risk: LOW (after mitigations)
Risk Level: MEDIUM
Attack Scenario:
- Attacker gains access to server with elevated privileges
- Attacker dumps PHP process memory or debugs running process
- Encryption keys and decrypted tokens extracted from memory
Reality Check:
- ❌ Session encryption DOES NOT protect against this
- Tokens are plaintext in memory during request processing
- Encryption only protects data at rest on disk
Mitigation:
-
Operating system security (primary defense):
- Restrict SSH access (key-based only)
- Disable root login
- Use firewalls (only open necessary ports)
- Keep OS patched
-
Principle of least privilege:
- Web server runs as unprivileged user
- No shell access for web server user
- Restrict sudo access
-
Process isolation:
- Use containers (Docker) or VMs
- SELinux or AppArmor profiles
- PHP-FPM pools per application
Residual Risk: MEDIUM (OS compromise is severe regardless)
Risk Level: HIGH
Attack Scenario:
- Attacker injects malicious JavaScript into your application
- JavaScript steals session cookie or makes API calls as user
- Encryption doesn't prevent this (attack happens client-side)
Reality Check:
- ❌ Session encryption DOES NOT protect against XSS
- XSS attacks happen in the browser, not on the server
- Attacker can make authenticated API calls directly
Mitigation:
-
HTTPOnly cookies (prevent cookie theft):
session.cookie_httponly = 1 -
Content Security Policy (prevent script injection):
header("Content-Security-Policy: default-src 'self'; script-src 'self';");
-
Output escaping (prevent HTML injection):
echo htmlspecialchars($user_input, ENT_QUOTES, 'UTF-8');
-
Input validation:
$clean_input = filter_var($input, FILTER_SANITIZE_STRING);
Residual Risk: MEDIUM (XSS is application-specific)
Rotate encryption keys:
- Every 90 days (recommended)
- Immediately if key compromise suspected
- After employee departure (if they had key access)
- After security incident
1. Generate new key:
NEW_KEY=$(php -r "echo base64_encode(random_bytes(32));")
echo "New key: $NEW_KEY"2. Deploy new key alongside old key:
# Keep old key for backward compatibility
export FS_SESSION_ENCRYPTION_KEY_OLD="$FS_SESSION_ENCRYPTION_KEY"
export FS_SESSION_ENCRYPTION_KEY="$NEW_KEY"3. Update application to try new key first, fallback to old:
// During migration period
$keys = [
$_ENV['FS_SESSION_ENCRYPTION_KEY'], // New key (primary)
$_ENV['FS_SESSION_ENCRYPTION_KEY_OLD'] // Old key (fallback)
];
// Try decryption with each key
foreach ($keys as $key) {
$fs = new FamilySearch([
'sessionEncryptionKey' => $key,
'sessionEncryption' => true
]);
// If successful, break
}4. Wait for migration period (7-30 days):
- Users gradually re-authenticate
- Old sessions expire naturally
- New sessions use new key
5. Remove old key:
unset FS_SESSION_ENCRYPTION_KEY_OLDIf you discover a security vulnerability in this SDK, please report it responsibly:
DO:
- ✅ SDK Issues: Email security issues privately to devsupport@familysearch.org
- ✅ FamilySearch Platform Issues: Report through FamilySearch Developer Support
- ✅ Provide detailed steps to reproduce
- ✅ Include proof-of-concept code (if applicable)
- ✅ Give us reasonable time to fix (90 days)
DON'T:
- ❌ Publicly disclose vulnerabilities before fix is released
- ❌ Exploit vulnerabilities in production systems
- ❌ Demand payment for vulnerability disclosure
Please include:
- SDK version affected
- Description of vulnerability
- Steps to reproduce
- Proof-of-concept code
- Potential impact assessment
- Suggested fix (if applicable)
Example Report:
Subject: [SECURITY] Session Token Exposure via [vector]
SDK Version: 1.2.0
Description:
Under certain conditions, access tokens may be logged in plaintext
when [specific scenario].
Steps to Reproduce:
1. Enable debug logging
2. Perform OAuth flow
3. Check logs at /var/log/php-errors.log
Impact:
Access tokens exposed in log files, potential unauthorized API access.
Proof of Concept:
[code here]
Suggested Fix:
Mask tokens in debug output using [approach].
We aim to:
- Acknowledge your report within 48 hours
- Provide initial assessment within 7 days
- Release a fix within 90 days (or explain delay)
- Credit you in release notes (if desired)
- Confirmed vulnerability → We create private security advisory
- Fix developed → Tested and reviewed
- Fix released → Published with CVE (if applicable)
- Public disclosure → After users have time to update
- Secrets scanning: git-secrets
- Dependency scanning: composer audit
- PHP security: Snyk, OWASP Dependency-Check
- OWASP PHP Security Cheat Sheet
- PHP Session Security
- NIST Cryptographic Standards
- FamilySearch API Documentation
Last Updated: 2026-08-12
SDK Version: 1.3.0+
Encryption Feature: Since v1.3.0