diff --git a/src/Rules/AuthenticationRule.php b/src/Rules/AuthenticationRule.php index 3b2eea7..e00b897 100644 --- a/src/Rules/AuthenticationRule.php +++ b/src/Rules/AuthenticationRule.php @@ -41,6 +41,7 @@ class AuthenticationRule extends BaseRule 'private_key', 'secret_key', ]; + /** @var array Patterns that indicate i18n/message keys (not credentials) */ private const I18N_KEY_PATTERNS = [ '/^[a-z_]+\.[a-z_]+/', // dot notation like "auth.password" @@ -505,6 +506,11 @@ class AuthenticationRule extends BaseRule if ($item->value instanceof Node\Scalar\String_) { $value = $item->value->value; + // Skip empty values + if (empty($value)) { + continue; + } + // Skip Laravel validation rules if ($this->isLaravelValidationRule($value)) { continue; @@ -515,7 +521,15 @@ class AuthenticationRule extends BaseRule continue; } - if (!empty($value) && !$this->isPlaceholder($value) && !$this->isDescriptiveMessage($value)) { + // Skip placeholder values + if ($this->isPlaceholder($value)) { + continue; + } + + // Core check: does the VALUE look like an actual credential? + // This is the fundamental approach - analyze the value itself, + // not just the key name + if ($this->looksLikeActualCredential($value)) { $vulnerabilities[] = $this->createVulnerability( $this->msg('auth.name'), Vulnerability::SEVERITY_CRITICAL, @@ -645,15 +659,30 @@ class AuthenticationRule extends BaseRule */ private function isPlaceholder(string $value): bool { - $placeholders = [ - 'xxx', 'password', 'secret', 'changeme', 'your_', - 'your-', '<', '>', '{', '}', 'TODO', 'FIXME', - 'env(', 'config(', 'getenv', + $lower = strtolower($value); + + // Exact placeholder values (common development placeholders) + $exactPlaceholders = [ + 'xxx', 'xxxx', 'xxxxx', 'password', 'secret', 'changeme', + 'change_me', 'changethis', 'your_password', 'your_secret', + 'test', 'testing', 'example', 'sample', 'demo', 'dummy', + 'null', 'none', 'empty', 'undefined', ]; - $lower = strtolower($value); - foreach ($placeholders as $placeholder) { - if (str_contains($lower, strtolower($placeholder))) { + if (in_array($lower, $exactPlaceholders)) { + return true; + } + + // Contains template syntax or environment references + $templatePatterns = [ + 'your_', 'your-', '<', '>', '{', '}', '${', + 'TODO', 'FIXME', 'CHANGEME', + 'env(', 'config(', 'getenv', 'process.env', + '_here', '_HERE', '-here', '-HERE', + ]; + + foreach ($templatePatterns as $pattern) { + if (str_contains($value, $pattern) || str_contains($lower, strtolower($pattern))) { return true; } } @@ -723,56 +752,82 @@ class AuthenticationRule extends BaseRule } /** - * Check if value looks like a descriptive message rather than a credential + * Check if a value looks like an actual credential (not a label, message, or config value) + * + * This is the core detection logic: even if the KEY suggests a credential, + * we only flag it if the VALUE actually looks like a credential. + * + * Credential characteristics: + * - Relatively short string (typically < 100 chars) + * - No or few spaces (credentials are compact) + * - ASCII characters (or base64/hex encoded) + * - Not a human-readable sentence + * + * Non-credential (config/label) characteristics: + * - Contains spaces (human-readable text) + * - Contains non-ASCII (Japanese, etc.) - display text + * - Ends with punctuation (sentence) + * - Contains common message/label words */ - private function isDescriptiveMessage(string $value): bool + private function looksLikeActualCredential(string $value): bool { - // Strings with multiple spaces are likely descriptions/messages + // Empty or very short values are not credentials + if (strlen($value) < 3) { + return false; + } + + // Very long strings are unlikely to be credentials (probably text content) + if (strlen($value) > 200) { + return false; + } + + // Contains non-ASCII characters (Japanese, Chinese, etc.) - likely display text + if (preg_match('/[^\x00-\x7F]/', $value)) { + return false; + } + + // Contains multiple spaces - likely a sentence or description if (substr_count($value, ' ') >= 2) { - return true; + return false; } - // Strings ending with punctuation are likely messages - if (preg_match('/[.!?。!?]$/', $value)) { - return true; + // Ends with sentence punctuation - likely a message + if (preg_match('/[.!?]$/', $value)) { + return false; } - // Contains typical message indicators + // Contains common message/label indicators $messageIndicators = [ - 'してください', // Japanese polite request - 'ください', // Japanese please - 'です。', // Japanese sentence ending - 'ます。', // Japanese sentence ending - 'ません', // Japanese negative - 'please', - 'should', - 'must', - 'warning', - 'error', - 'invalid', - 'expired', - 'required', - 'missing', - 'detected', - 'found', - 'failed', - 'success', - 'unable', - 'cannot', - 'not found', - 'not allowed', - 'use ', - 'Use ', + 'please', 'should', 'must', 'warning', 'error', 'invalid', + 'expired', 'required', 'missing', 'detected', 'found', 'failed', + 'success', 'unable', 'cannot', 'not found', 'not allowed', + 'the ', 'The ', 'a ', 'A ', 'an ', 'An ', 'is ', 'are ', 'was ', + 'has ', 'have ', 'will ', 'would ', 'could ', 'can ', + 'your ', 'Your ', 'this ', 'This ', ]; $lower = strtolower($value); foreach ($messageIndicators as $indicator) { if (str_contains($value, $indicator) || str_contains($lower, strtolower($indicator))) { - return true; + return false; } } - return false; + // If it looks like a typical credential format, it's suspicious + // Credentials are usually alphanumeric with possible special chars, no spaces + if (preg_match('/^[a-zA-Z0-9_\-+=\/\.@#$%^&*!]+$/', $value)) { + // Looks like a credential format + return true; + } + + // Single word with a space (like "Reset Password") is likely a label + if (substr_count($value, ' ') === 1 && preg_match('/^[A-Z][a-z]+ [A-Z][a-z]+$/', $value)) { + return false; + } + + // Default: if it passed all the "not a credential" checks, consider it suspicious + // But only if it doesn't contain spaces (credentials typically don't have spaces) + return !str_contains($value, ' '); } /** diff --git a/src/Rules/InsecureConfigRule.php b/src/Rules/InsecureConfigRule.php index c0576f4..8becc64 100644 --- a/src/Rules/InsecureConfigRule.php +++ b/src/Rules/InsecureConfigRule.php @@ -551,6 +551,21 @@ class InsecureConfigRule extends BaseRule if ($item->value instanceof Node\Scalar\String_) { $val = $item->value->value; + // Skip empty values + if (empty($val)) { + continue; + } + + // Skip language/translation files (values are field labels, not secrets) + if ($this->isLanguageFile($filePath)) { + continue; + } + + // Skip if value looks like a translation (non-ASCII characters) + if ($this->isTranslationString($val)) { + continue; + } + // Skip i18n message keys if ($this->isI18nKey($originalKey)) { continue; @@ -566,8 +581,18 @@ class InsecureConfigRule extends BaseRule continue; } + // Skip placeholder values + if ($this->isPlaceholderValue($val)) { + continue; + } + + // Core check: does the VALUE look like an actual credential? + if (!$this->looksLikeActualCredential($val)) { + continue; + } + // Check if it's not using env() - if (!empty($val) && !str_starts_with($val, 'env(')) { + if (!str_starts_with($val, 'env(')) { $vulnerabilities[] = $this->createVulnerability( $this->msg('config.name'), Vulnerability::SEVERITY_CRITICAL, @@ -652,6 +677,120 @@ class InsecureConfigRule extends BaseRule return false; } + /** + * Check if file is a language/translation file + */ + private function isLanguageFile(string $filePath): bool + { + // Common Laravel/PHP language file paths + $langPatterns = [ + '#/lang/[a-z]{2}(_[A-Z]{2})?/#', // /lang/ja/, /lang/en/, /lang/zh_CN/ + '#/locales?/[a-z]{2}(_[A-Z]{2})?/#', // /locale/ja/, /locales/en/ + '#/resources/lang/#', // Laravel's resources/lang/ + '#/translations?/#', // /translation/, /translations/ + ]; + + foreach ($langPatterns as $pattern) { + if (preg_match($pattern, $filePath)) { + return true; + } + } + + return false; + } + + /** + * Check if value looks like a translation string (not a credential) + */ + private function isTranslationString(string $value): bool + { + if (empty($value)) { + return false; + } + + // Contains non-ASCII characters (Japanese, Chinese, Korean, etc.) + if (preg_match('/[^\x00-\x7F]/', $value)) { + return true; + } + + // Common translation patterns: :attribute placeholder, readable text with spaces + if (preg_match('/:\w+/', $value)) { // :attribute, :max, etc. + return true; + } + + return false; + } + + /** + * Check if value is a placeholder (not an actual credential) + */ + private function isPlaceholderValue(string $value): bool + { + $lower = strtolower($value); + + // Exact placeholder values + $exactPlaceholders = [ + 'xxx', 'xxxx', 'xxxxx', 'password', 'secret', 'changeme', + 'change_me', 'changethis', 'your_password', 'your_secret', + 'test', 'testing', 'example', 'sample', 'demo', 'dummy', + 'null', 'none', 'empty', 'undefined', + ]; + + if (in_array($lower, $exactPlaceholders)) { + return true; + } + + // Contains template syntax or environment references + $templatePatterns = [ + 'your_', 'your-', '<', '>', '{', '}', '${', + 'TODO', 'FIXME', 'CHANGEME', + 'env(', 'config(', 'getenv', 'process.env', + '_here', '_HERE', '-here', '-HERE', + ]; + + foreach ($templatePatterns as $pattern) { + if (str_contains($value, $pattern) || str_contains($lower, strtolower($pattern))) { + return true; + } + } + + return false; + } + + /** + * Check if a value looks like an actual credential + */ + private function looksLikeActualCredential(string $value): bool + { + // Very short values are not credentials + if (strlen($value) < 3) { + return false; + } + + // Very long strings are unlikely to be credentials + if (strlen($value) > 200) { + return false; + } + + // Contains non-ASCII characters - likely display text + if (preg_match('/[^\x00-\x7F]/', $value)) { + return false; + } + + // Contains multiple spaces - likely a sentence + if (substr_count($value, ' ') >= 2) { + return false; + } + + // Credentials are usually alphanumeric with possible special chars, no spaces + if (preg_match('/^[a-zA-Z0-9_\-+=\/\.@#$%^&*!]+$/', $value)) { + return true; + } + + // Default: suspicious if no spaces + return !str_contains($value, ' '); + } + /** * Check config file return statements */ diff --git a/src/Rules/XssRule.php b/src/Rules/XssRule.php index 42e1ae8..77fdff1 100644 --- a/src/Rules/XssRule.php +++ b/src/Rules/XssRule.php @@ -211,42 +211,49 @@ class XssRule extends BaseRule } } - // JavaScript context - {{ }} in