This document describes frontend-specific secret patterns that should be detected by the secret scanner. Frontend secrets are particularly dangerous because they ship to end users and can be extracted from distributed applications (especially Electron apps).
Risk Level: HIGH
Browser storage APIs are commonly used to store authentication tokens and API keys. While often used legitimately (storing user tokens after OAuth flow), hardcoded values in these calls indicate accidental credential exposure.
Pattern:
localStorage.setItem('apiKey', 'sk-1234567890abcdef...')
localStorage.setItem('token', 'ghp_1234567890abcdef...')Detection Regex:
localStorage\.setItem\s*\(\s*["\'](?:api[_-]?key|apikey|token|access[_-]?token|auth[_-]?token|secret|api_secret|bearer)["\']\s*,\s*["\']([a-zA-Z0-9_-]{20,})["\']Key indicators:
- Method:
localStorage.setItem - Key names: apiKey, token, accessToken, authToken, secret, api_secret, bearer
- Value: 20+ character alphanumeric string
Pattern:
sessionStorage.setItem('sessionToken', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...')Detection Regex:
sessionStorage\.setItem\s*\(\s*["\'](?:api[_-]?key|apikey|token|access[_-]?token|auth[_-]?token|secret|api_secret|bearer)["\']\s*,\s*["\']([a-zA-Z0-9_-]{20,})["\']Key indicators:
- Method:
sessionStorage.setItem - Same key names as localStorage
- Value: 20+ character alphanumeric string
Risk Level: HIGH
Web applications often expose configuration through global window objects. These can accidentally contain secrets instead of using environment variables.
Pattern:
window.config = {
apiKey: 'sk-1234567890abcdef...',
apiSecret: 'secret_1234567890'
}
window.config.apiKey = 'sk-1234567890abcdef...'Detection Regex (property assignment):
window\.(?:config|CONFIG|appConfig|APP_CONFIG)(?:\.\s*(?:api[_-]?key|apikey|token|access[_-]?token|auth[_-]?token|secret|api_secret|bearer|aws[_-]?key)\s*)?[:=]\s*["\']([a-zA-Z0-9_-]{16,})["\']Detection Regex (object literal):
window\.(?:config|CONFIG|appConfig|APP_CONFIG)\s*=\s*{[^}]*?(?:api[_-]?key|apikey|token|access[_-]?token|auth[_-]?token|secret|api_secret|bearer|aws[_-]?key)\s*:\s*["\']?([a-zA-Z0-9_-]{16,})["\']?Key indicators:
- Property names: config, CONFIG, appConfig, APP_CONFIG
- Direct assignment or object property assignment
- 16+ character alphanumeric string
Risk Level: MEDIUM
Vite applications use import.meta.env to access environment variables. The variable reference itself is safe, but hardcoded values should still be caught.
Safe Pattern (False Positive):
const apiKey = import.meta.env.VITE_API_KEY; // Safe - reads from .envUnsafe Pattern (Should be caught by generic patterns):
const apiKey = 'sk-1234567890abcdef...'; // Should be caughtDetection:
- The scanner has a specific pattern to flag
import.meta.envusage for review - False positive filter allows safe
import.meta.env.VARIABLE_NAMEreferences - Generic patterns should catch actual hardcoded values
False Positive Pattern:
import\.meta\.env\.[A-Z_]+Risk Level: HIGH
Electron main process can access Node.js environment variables through process.env.
Safe Pattern (False Positive):
const dsn = process.env.SENTRY_DSN; // Safe - reads from environmentUnsafe Pattern:
process.env.API_KEY = 'sk-1234567890abcdef...'; // Hardcoded assignmentDetection Regex:
process\.env\.[A-Z_]+\s*=\s*["\']([a-zA-Z0-9_-]{20,})Read vs. Write Detection:
The scanner distinguishes between safe reads and unsafe assignments using regex:
- Assignment (unsafe):
process\.env\.[A-Z_]+\s*=[^=]— single=not followed by another= - Read (safe):
process\.env\.[A-Z_]+(\s|\)|,|;|$)— variable used in expression context
This correctly handles comparison operators (==, ===) which are not assignments.
Risk Level: LOW
Build tools often replace variables at build time using define options. These appear as global variables:
Pattern:
const dsn = __SENTRY_DSN__; // Vite define replacementDetection:
- Pattern:
__VARIABLE_NAME__format - These are generally safe if values come from CI/CD secrets
- False positive filter:
__[\w_]+__
Risk Level: CRITICAL
Environment files should never be committed to git. The scanner detects .env files with actual secret values.
Pattern:
# .env file
API_KEY=sk-1234567890abcdef
DATABASE_URL=postgresql://user:password@localhost/dbDetection Regex:
^[A-Z_]+=\s*["\']?([a-zA-Z0-9_-]{20,})["\']?\s*$Note: The scanner checks if the file being scanned is an actual .env file and flags any non-empty, non-comment lines with 20+ character values.
Risk Level: HIGH
Firebase web apps include configuration objects with API keys. While Firebase keys are technically "public" (they're meant to be exposed), they should be loaded from environment variables in production.
Pattern:
const firebaseConfig = {
apiKey: "AIzaSyD1234567890abcdef",
authDomain: "app.firebaseapp.com",
projectId: "app-123"
};Detection Regex:
firebaseConfig\s*=\s*{[^}]*apiKey\s*:\s*["\']([a-zA-Z0-9_-]{20,})["\']Key indicators:
- Variable name:
firebaseConfig - Contains
apiKeyproperty - Google API key format:
AIza...
Risk Level: LOW
Google Analytics IDs are public identifiers, not secrets. However, they should be documented and tracked.
Pattern:
gtag('config', 'G-XXXXXXXXXX');Detection Regex:
\bG-[A-Z0-9]{10}\bNote: Uses word boundaries (\b) to avoid matching CSS classes like bg-background. These are generally safe to expose but should be loaded from environment variables for consistency.
Risk Level: HIGH
API calls with keys embedded in URL parameters instead of headers.
Pattern:
fetch('https://api.service.io/endpoint?api_key=sk-1234567890abcdef')Detection Regex:
https?://[^\s"\']*api[_-]?key[_-]?=?[a-zA-Z0-9_-]{20,}Key indicators:
- URL with
api_key=orapikey=parameter - 20+ character alphanumeric value
The scanner applies patterns line by line, not against entire file content. Each line is tested against all compiled patterns using re.finditer() with re.IGNORECASE. Anchors like ^ and $ in patterns (e.g., the .env variable pattern) are evaluated relative to each individual line.
For the canonical source of truth, refer to the pattern definitions in apps/backend/security/scan_secrets.py (FRONTEND_PATTERNS, GENERIC_PATTERNS, etc.). This document mirrors those implementations and should be updated whenever the code changes.
When creating test cases for frontend patterns:
-
Create realistic frontend code samples:
- TypeScript files for Electron/Vite apps
- JSX/TSX files for React components
- Plain JavaScript files
-
Test both positive and negative cases:
- Positive: Hardcoded secrets that should be detected
- Negative: Legitimate env variable usage that should NOT be detected
-
Use fake secret builders to avoid triggering push protection:
# Build test secrets via concatenation
_TEST_OPENAI_KEY = "sk-" + "1234567890abcdefghijklmnop"
_TEST_GOOGLE_KEY = "AIza" + "012345678901234567890123456789012345"- Example test cases:
// Should be detected
localStorage.setItem('apiKey', 'sk-12345678901234567890');
// Should NOT be detected (false positive - safe env usage)
const apiKey = import.meta.env.VITE_API_KEY;
// Should be detected
window.config = {
apiKey: 'sk-12345678901234567890'
};
// Should NOT be detected (safe process.env reference)
const dsn = process.env.SENTRY_DSN;Frontend patterns are checked BEFORE generic patterns to provide more specific error messages. The current order in ALL_PATTERNS is:
- FRONTEND_PATTERNS (most specific)
- GENERIC_PATTERNS (catch-all)
- SERVICE_PATTERNS (known formats)
- PRIVATE_KEY_PATTERNS (cryptographic)
- DATABASE_PATTERNS (connection strings)
Placeholder patterns use word boundaries or context-aware matching to avoid false negatives:
\bmock\b,\bfixture\b,\bdummy\b,\bfake\b— word boundaries prevent matching inside identifiers(?<![\w.-])example(?![\w.-])— excludes domains likeapi.example.com(?<![\w-])abc123(?![\w-])— excludes token substrings separated by hyphens
The scanner normalizes file paths to POSIX format (forward slashes) before checking ignore patterns. This ensures Windows backslash paths (C:\project\node_modules\...) correctly match ignore rules like node_modules/.
Frontend patterns currently apply to all scanned files. Future improvement could scope them to frontend file types:
.js,.jsx,.ts,.tsx— JavaScript/TypeScript files.vue— Vue components.svelte— Svelte components.env,.env.local,.env.production— Environment files
Vite:
- Uses
import.meta.envfor build-time constants - Build defines create
__VARIABLE__globals - Environment files:
.env,.env.local,.env.production
Create React App:
- Uses
process.env(injected by webpack) - Environment files:
.env,.env.local,.env.production
Next.js:
- Uses
process.envfor server-side - Uses
import.meta.envfor client-side (Vite-based) - Environment files:
.env.local,.env.development
Electron:
- Main process:
process.env(Node.js) - Renderer:
import.meta.env(Vite) - Preload:
process.env(Node.js context)
-
localStorage/sessionStorage with hardcoded tokens
- Often happens during prototyping
- Developers forget to remove before commit
-
window.config in index.html
- Inline scripts with configuration
- Should use template variables instead
-
.env files committed to git
.envshould be in.gitignore- Only
.env.exampleshould be committed
-
API endpoints with embedded keys
- URL parameters instead of headers
- Keys visible in browser history/logs
- Hardcoded credentials in bundle: Even if secrets are removed from source, they may remain in built bundles
- Debug logging:
console.log(apiKey)statements expose secrets in browser console - Error messages: Stack traces may include sensitive URLs with keys
These patterns are not currently implemented but could be added in the future:
-
Axios/Fetch interceptors with hardcoded auth:
axios.interceptors.request.use(config => { config.headers.Authorization = 'Bearer sk-1234...'; // Hardcoded token return config; });
-
React Context with secrets:
const AuthContext = createContext({ apiKey: 'sk-1234...' // Direct value });
-
Vue/Vuex stores with secrets:
export default new Vuex.Store({ state: { apiKey: 'sk-1234...' // Hardcoded } });
-
GraphQL clients with hardcoded tokens:
const client = new ApolloClient({ uri: 'https://api.service.io/graphql', headers: { 'Authorization': 'Bearer sk-1234...' } // Hardcoded });
-
Service Worker registration with tokens:
navigator.serviceWorker.register('/sw.js?token=sk-1234...'); // Token in URL
Frontend secret detection requires understanding the unique patterns used in web applications. The key challenge is balancing security (detecting real leaks) with usability (avoiding false positives for legitimate env variable usage).
The patterns documented here focus on:
- High-risk storage APIs (localStorage/sessionStorage)
- Common configuration patterns (window.config)
- Framework-specific env access (import.meta.env, process.env)
- Third-party service configs (Firebase, Google Analytics)
- Embedded secrets in API endpoints
All patterns follow the existing scan_secrets.py convention of tuple pairs: (regex_pattern, description).