wp-security-secrets — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited wp-security-secrets (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
A focused review for the secrets layer — what's stored, how it's generated, how it's compared, how it leaks. Narrow scope by design; run alongside wp-security-audit and wp-security-deep.
Trigger when:
GitHub etc.) — look for stored credentials.
rand, mt_rand, uniqid, md5, sha1,password_hash, password_verify, setcookie, wp_generate_password, random_bytes, random_int, openssl_random_pseudo_bytes, update_option.*api_key, update_option.*secret, define.*KEY, define.*SECRET.
// HIGH — anyone with repo read access has prod credentials
const STRIPE_SECRET = 'sk_live_4eC39HqLyjWDarjtT1...';
$api_key = 'AIzaSy...';
define( 'MYPLUGIN_API_TOKEN', 'ghp_xxx...' );Fix: load from wp-config.php constants (server-deployed, not in repo), an option (get_option) with admin-only write, or an env var via getenv(). Provide a settings page for the user to enter their own.
Search patterns:
sk_, pk_live_,AKIA, ghp_, xoxb-, AIza, Bearer , eyJ (JWT), hex strings 32+ chars.
define( '...KEY...', define( '...SECRET...', define( '...TOKEN...'with a non-empty literal.
[A-Za-z0-9+/]{40,}={0,2}).Flag any hit. Even "test" keys count — they get committed to prod.
// HIGH — predictable
$token = md5( uniqid() );
$reset_key = mt_rand();
$session_id = uniqid( '', true );rand, mt_rand, uniqid are NOT cryptographically secure. They're seeded predictably and the output can be reconstructed.
Fix — use one of:
// WP-native, seeded from random_bytes internally
$token = wp_generate_password( 32, false ); // alnum
$token = wp_generate_password( 64, true, true ); // alnum + special
// PHP-native
$token = bin2hex( random_bytes( 32 ) ); // 64 hex chars
$num = random_int( 100000, 999999 ); // 6-digit OTP
// WP nonce (for CSRF only — not a long-lived token!)
$nonce = wp_create_nonce( 'action' );Don't confuse:
wp_create_nonce is a 10-char CSRF token, valid 12-24h. NOT forpassword resets, API keys, session IDs.
wp_generate_uuid4() is NOT cryptographically random despite thev4 UUID name. WordPress core implements it with mt_rand() (verified in wp-includes/functions.php). Use it for non-security identifiers (post UIDs, log IDs) only. For security tokens use wp_generate_password(), random_bytes(), or random_int().
// HIGH — broken
$hash = md5( $password );
$hash = sha1( $password . SALT );
$hash = crypt( $password ); // weak default algoWordPress provides wp_hash_password() / wp_check_password() — use these for WP user accounts. Since WP 6.8 the default algorithm is bcrypt (was PHPass / portable hash before). To work around bcrypt's 72-byte input limit, WP first HMAC-SHA384 pre-hashes the password, base64-encodes the result, then runs password_hash() with PASSWORD_BCRYPT, prefixing the output with $wp to distinguish it from vanilla bcrypt. wp_check_password() still verifies legacy PHPass hashes from older sites and rehashes on successful login, so the migration is transparent — don't reimplement password storage for WP users, always go through these functions.
The algorithm and options are filterable (wp_hash_password_algorithm, wp_hash_password_options) for sites that want argon2id or stronger bcrypt cost — but a plugin should not change site-wide defaults unless explicitly asked.
For non-WP-user passwords (custom auth, API client secrets):
$hash = password_hash( $password, PASSWORD_DEFAULT );
// later
if ( password_verify( $input, $hash ) ) { /* ok */ }
if ( password_needs_rehash( $hash, PASSWORD_DEFAULT ) ) {
// rehash and update store
}PASSWORD_DEFAULT resolves to bcrypt across all currently shipped PHP versions and may change in a future major PHP release — that's the point of the constant. Always pair it with password_needs_rehash() on verification to migrate stored hashes when the algorithm changes. Allocate at least 255 bytes for the stored hash column to leave room for future algorithm output growth. Never roll your own with hash() + salt.
// MEDIUM/HIGH — readable by JS, sent over HTTP, sent cross-site
setcookie( 'session', $token, time() + 3600 );Fix:
setcookie( 'myplugin_session', $token, [
'expires' => time() + 3600,
'path' => COOKIEPATH ?: '/',
'domain' => COOKIE_DOMAIN ?: '',
'secure' => is_ssl(),
'httponly' => true,
'samesite' => 'Lax', // or 'Strict' for sensitive flows
] );For WP's own auth, don't roll your own — use wp_set_auth_cookie which handles all flags correctly.
Flag: any setcookie storing a token, session ID, or user identifier without secure (when is_ssl()) and httponly.
// MEDIUM — secrets in error_log, often shipped to log aggregators
error_log( 'API request: ' . print_r( $request, true ) ); // includes auth header
var_dump( $_SERVER ); // HTTP_AUTHORIZATION leaks
WP_CLI::log( "Token: $token" );Flag:
error_log, var_dump, print_r, var_export over variablesnamed *token*, *key*, *secret*, *password*, *auth*, $_SERVER, full request bodies, or response objects with auth headers.
WP_DEBUG_LOG enabled in production code path (not a check, butwarn if define( 'WP_DEBUG_LOG', true ) appears in plugin code — shouldn't be set by a plugin).
Fix: redact before logging:
$safe = $request;
unset( $safe['headers']['Authorization'] );
$safe['body'] = '<redacted>';
error_log( wp_json_encode( $safe ) );Both are common — guidance:
rarely-changed secrets. Not in DB, not in repo, only readable by PHP. Document this in plugin readme.
settings page. Acceptable, but:
autoload = false if not needed every request.manage_options.wp_send_json or echo the option value back to non-admins.PAT). Never for site-wide secrets.
Flag: site-wide secrets stored in user meta, or settings pages without a capability check.
Cross-reference with wp-security-deep check #8: any secret comparison must use hash_equals( $stored, (string) $given ). Never == or ===.
reset / session token, password stored with md5/sha1, missing HttpOnly on session cookie.
for non-secret IDs that gain meaning later.
autoload = false, no rotationstory documented.
Same as wp-security-audit. If hardcoded secrets are found, the report MUST include a top-line warning recommending:
git filter-repo orBFG) — or accept that the secret is permanently compromised in the repo.
wp-config.php constant or settings option.State this even when the user only asked for a code review — leaked secrets need real-world action, not just a code change.
assignment, file include, mail/zip injection, type juggling, race.
envelope encryption).
gitleaks, trufflehog).https://www.php.net/manual/en/function.password-hash.php
random_bytes:https://www.php.net/manual/en/function.random-bytes.php
wp_generate_password:https://developer.wordpress.org/reference/functions/wp_generate_password/
https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.