Skip to content

Security: FamilySearch/fs-php-lite

docs/SECURITY.md

Security Policy

Supported Versions

  • 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.


Security Overview

What the SDK Provides

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)

What You Are Responsible For

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

Session Token Storage

Default Behavior: Plaintext Storage

⚠️ Warning: By default, OAuth access tokens are stored in plaintext in PHP session files.

// 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

Secure Configuration: Encrypted Storage

✅ Recommended: Enable AES-256-GCM encryption for production:

$fs = new FamilySearch([
    'appKey' => $_ENV['FS_APP_KEY'],
    'sessionEncryption' => true,
    'sessionEncryptionKey' => $_ENV['FS_SESSION_ENCRYPTION_KEY']
]);

What Encryption Protects Against

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)

What Encryption Does NOT Protect Against

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.


Encryption Best Practices

1. Generate Secure Encryption Keys

✅ 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

2. Store Keys in Environment Variables

✅ 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
]);

3. Use Secure Key Storage Solutions

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
]);

4. Key Rotation Strategy

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
];

5. Environment-Specific Keys

✅ 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.


Server Configuration

1. PHP Session Directory Permissions

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!

2. PHP Session Configuration

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 = 6

Apply configuration changes:

# Apache
sudo systemctl restart apache2

# Nginx + PHP-FPM
sudo systemctl restart php8.x-fpm
sudo systemctl restart nginx

3. Verify Session Security

Test 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";
}
?>

4. Enforce HTTPS

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
}

Production Deployment Checklist

Use this checklist before deploying to production:

Application Security

  • 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

Server Configuration

  • 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

Monitoring & Maintenance

  • 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

Testing

  • 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

Threat Model

Threat: Filesystem Access to Session Files

Risk Level: HIGH

Attack Scenario:

  • Attacker gains read access to server filesystem
  • Session files in /var/lib/php/sessions are readable
  • Attacker extracts plaintext OAuth tokens from session files

Mitigation:

  1. Enable encryption (primary defense):

    'sessionEncryption' => true,
    'sessionEncryptionKey' => $_ENV['FS_SESSION_ENCRYPTION_KEY']
  2. Set proper file permissions (defense-in-depth):

    chmod 700 /var/lib/php/sessions
  3. 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)


Threat: Session Hijacking

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:

  1. Enforce HTTPS (prevent network interception):

    session.cookie_secure = 1
  2. HTTPOnly cookies (prevent XSS theft):

    session.cookie_httponly = 1
  3. SameSite cookies (prevent CSRF):

    session.cookie_samesite = "Strict"
  4. Regenerate session ID after login:

    // After successful authentication
    session_regenerate_id(true);
  5. 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)


Threat: Encryption Key Exposure

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:

  1. Never commit keys to version control:

    # Add to .gitignore
    echo ".env" >> .gitignore
    echo "config/secrets.php" >> .gitignore
  2. Use environment variables:

    export FS_SESSION_ENCRYPTION_KEY="<key>"
  3. Use secrets management (production):

    • AWS Secrets Manager
    • HashiCorp Vault
    • Azure Key Vault
  4. Scan for leaked secrets:

    # Use git-secrets or similar
    git secrets --install
    git secrets --register-aws

Residual Risk: LOW (after mitigations)


Threat: Memory Dumps / Process Inspection

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:

  1. Operating system security (primary defense):

    • Restrict SSH access (key-based only)
    • Disable root login
    • Use firewalls (only open necessary ports)
    • Keep OS patched
  2. Principle of least privilege:

    • Web server runs as unprivileged user
    • No shell access for web server user
    • Restrict sudo access
  3. Process isolation:

    • Use containers (Docker) or VMs
    • SELinux or AppArmor profiles
    • PHP-FPM pools per application

Residual Risk: MEDIUM (OS compromise is severe regardless)


Threat: XSS (Cross-Site Scripting) Attacks

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:

  1. HTTPOnly cookies (prevent cookie theft):

    session.cookie_httponly = 1
  2. Content Security Policy (prevent script injection):

    header("Content-Security-Policy: default-src 'self'; script-src 'self';");
  3. Output escaping (prevent HTML injection):

    echo htmlspecialchars($user_input, ENT_QUOTES, 'UTF-8');
  4. Input validation:

    $clean_input = filter_var($input, FILTER_SANITIZE_STRING);

Residual Risk: MEDIUM (XSS is application-specific)


Key Rotation Procedure

When to Rotate Keys

Rotate encryption keys:

  • Every 90 days (recommended)
  • Immediately if key compromise suspected
  • After employee departure (if they had key access)
  • After security incident

Rotation Steps

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_OLD

Reporting Security Issues

Responsible Disclosure

If 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

What to Include in Report

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].

Response Timeline

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)

Security Advisory Process

  1. Confirmed vulnerability → We create private security advisory
  2. Fix developed → Tested and reviewed
  3. Fix released → Published with CVE (if applicable)
  4. Public disclosure → After users have time to update

Additional Resources

Security Tools

Security References

PHP Security Configuration


Last Updated: 2026-08-12
SDK Version: 1.3.0+
Encryption Feature: Since v1.3.0

There aren't any published security advisories