Security-only release per ADR-0001. Closes all six ship-blockers from the post-v1.5.0 audit: PSEC-001 (no rate limiting), PSEC-002 (account enumeration via separate error messages), PSEC-003 (registration auto-login with no email verification), PSEC-004 (ungated "My Data" templates), PSEC-005 (CSV formula injection on subscriber export), PSEC-006 (no honeypot/CAPTCHA on public forms). Adds inc/config.php (single source of truth for all security thresholds, filterable via peptide_starter_security_config), inc/rate-limiter.php (Peptide_Starter_Rate_Limiter + Cloudflare-aware client IP resolver, transient-backed, never stores raw IPs), and inc/email-verification.php (43-char token, 24h TTL, /verify rewrite, rate-limited resend endpoint). Auth handlers rewritten to: unify all failure messages, rate-limit before any expensive check, drop registrations with a filled honeypot silently, stop auto-login on register, send a verification email instead. The require_login() helper lives in inc/helpers.php and is applied at the top of page-tracker.php, page-subject-log.php, and page-protocol-builder.php; page-calculator.php stays open (no PII, no writes). Newsletter signup gains: consent checkbox, privacy link, honeypot, autoload=false on the wp_options blob, unsubscribe token stored per row, /newsletter-unsubscribe route, merged success+duplicate into a single "ok" state. Admin export runs every cell through peptide_starter_csv_safe() which prefixes leading =/+/-/@/tab/CR with a quote. Contact handler hardens against email-header injection (rejects name values with \r \n , < >), rate-limits, honors honeypot, tags mail failures with a request ID for correlation without logging message content. Adds Tools → Mail Test (inc/mail-diagnostic.php): a permanent wp_mail probe so we can check Hostinger outbound deliverability any time it looks flaky. Adds a one-time admin_init migration that enrols pre-v1.5.1 subscribers into the verification flow (currently a no-op — zero users — but guarded for the future). Other cleanup from the audit: fallback primary menu moved out of header.php into inc/helpers.php (redeclaration risk), placeholder footer language links replaced with "Translations coming soon", footer copyright now reads through the customizer getter, settings-panel gains a proper Tab/Shift+Tab focus trap and saves/restores body overflow, documentation heading IDs slugified from heading text with collision counters, JS files standardized on const/let. Six new PHPUnit suites cover rate limiter lifecycle, auth handler error unification and rate limits, email verification token generation and expiry, contact handler header injection + rate limit + honeypot, newsletter signup consent + CSV safety, and require_login gate behavior. ARCHITECTURE.md and CONVENTIONS.md updated in the same commit — new module layout, data flows, security patterns for rate-limiting, honeypotting, gating, and CSV-safe export. Version bumped to 1.5.1 in style.css and functions.php. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
123 lines
4.1 KiB
PHP
123 lines
4.1 KiB
PHP
<?php
|
|
/**
|
|
* Security and Feature Configuration
|
|
*
|
|
* Single source of truth for every security threshold, cooldown, and
|
|
* toggle used by the theme. All values are filterable via
|
|
* peptide_starter_security_config so deployments can override without
|
|
* forking code.
|
|
*
|
|
* @see inc/rate-limiter.php — consumes login/register/contact/newsletter limits
|
|
* @see inc/auth-handlers.php — consumes registration + email-verify settings
|
|
* @see inc/email-verification.php — consumes token TTL + resend limit
|
|
* @see inc/contact-handler.php — consumes contact rate limit
|
|
* @see functions.php — includes this file
|
|
*
|
|
* What: Returns the security configuration array for all abuse controls.
|
|
* Who calls it: Any handler needing a threshold; called once per request.
|
|
* Dependencies: None — pure array + filter.
|
|
*
|
|
* @package peptide-starter
|
|
*/
|
|
|
|
// Exit if accessed directly.
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Return the theme-wide security configuration.
|
|
*
|
|
* Every threshold in the theme must be read from this function; no magic
|
|
* numbers in handler files. Override with add_filter( 'peptide_starter_security_config', ... ).
|
|
*
|
|
* Cost note: this function is cheap and pure — no DB reads. Call it freely.
|
|
*
|
|
* @return array {
|
|
* @type array $rate_limits Per-action limit + window tuples.
|
|
* @type int $verify_token_ttl Seconds a verification token stays valid.
|
|
* @type int $verify_resend_limit Resends permitted per hour per IP+email.
|
|
* @type bool $email_verify_required Whether new registrations require email verification.
|
|
* @type bool $registration_enabled Master switch (separate from users_can_register).
|
|
* @type int $newsletter_autoload_threshold Subscribers before admin migration notice.
|
|
* @type array $contact_topics Allowlisted contact form topic slugs.
|
|
* @type int $username_min Minimum username length.
|
|
* @type int $username_max Maximum username length.
|
|
* @type int $password_min Minimum password length.
|
|
* }
|
|
*/
|
|
function peptide_starter_security_config() {
|
|
$defaults = array(
|
|
'rate_limits' => array(
|
|
// action => array( limit, window_seconds ).
|
|
'login' => array(
|
|
'limit' => 5,
|
|
'window' => 60,
|
|
),
|
|
'register' => array(
|
|
'limit' => 3,
|
|
'window' => HOUR_IN_SECONDS,
|
|
),
|
|
'contact' => array(
|
|
'limit' => 5,
|
|
'window' => HOUR_IN_SECONDS,
|
|
),
|
|
'newsletter' => array(
|
|
'limit' => 3,
|
|
'window' => HOUR_IN_SECONDS,
|
|
),
|
|
'verify_resend' => array(
|
|
'limit' => 2,
|
|
'window' => HOUR_IN_SECONDS,
|
|
),
|
|
),
|
|
'verify_token_ttl' => DAY_IN_SECONDS,
|
|
'email_verify_required' => true,
|
|
'registration_enabled' => true,
|
|
'newsletter_autoload_threshold' => 1000,
|
|
'contact_topics' => array( 'bug', 'feature', 'data', 'other' ),
|
|
'username_min' => 5,
|
|
'username_max' => 15,
|
|
'password_min' => 8,
|
|
);
|
|
|
|
/**
|
|
* Filter the theme's security configuration.
|
|
*
|
|
* @param array $defaults Default configuration.
|
|
*/
|
|
return apply_filters( 'peptide_starter_security_config', $defaults );
|
|
}
|
|
|
|
/**
|
|
* Shortcut to read a single top-level config key.
|
|
*
|
|
* @param string $key Top-level key (e.g. 'verify_token_ttl').
|
|
* @param mixed $fallback Value returned when the key is missing.
|
|
* @return mixed Configured value or $fallback.
|
|
*/
|
|
function peptide_starter_config_get( $key, $fallback = null ) {
|
|
$config = peptide_starter_security_config();
|
|
return array_key_exists( $key, $config ) ? $config[ $key ] : $fallback;
|
|
}
|
|
|
|
/**
|
|
* Read a rate limit tuple (limit, window) for a given action.
|
|
*
|
|
* @param string $action Action slug matching a key in rate_limits.
|
|
* @return array { 'limit' => int, 'window' => int } with safe defaults.
|
|
*/
|
|
function peptide_starter_config_rate_limit( $action ) {
|
|
$limits = peptide_starter_config_get( 'rate_limits', array() );
|
|
if ( isset( $limits[ $action ]['limit'], $limits[ $action ]['window'] ) ) {
|
|
return array(
|
|
'limit' => (int) $limits[ $action ]['limit'],
|
|
'window' => (int) $limits[ $action ]['window'],
|
|
);
|
|
}
|
|
return array(
|
|
'limit' => 5,
|
|
'window' => HOUR_IN_SECONDS,
|
|
);
|
|
}
|