CWE-547: Use of Hard-coded, Security-relevant Constants
Overview
MITRE scopes this weakness to code that "uses hard-coded constants instead of symbolic names for security-critical values, which increases the likelihood of mistakes during code maintenance or security policy change." The defect is not that a value is a literal; it is that the same security-relevant value is written out in several places, so changing the policy updates some of them and misses the rest. A session timeout of 1800 in four files becomes three files at the new value and one still at the old, and nothing fails loudly. The second, related problem is that a value which must differ between environments - an internal hostname, a port, a path - is compiled in, so it cannot change without a rebuild.
OWASP Classification
A02:2025 - Security Misconfiguration
Risk
Medium: MITRE records the consequence as quality degradation and reduced maintainability rather than a direct compromise. The realistic security impact is a partially applied policy change: a lockout threshold, token lifetime, or key length tightened everywhere the maintainer thought to look, and left at the old value on the path nobody searched. Secondary consequences are internal IPs and hostnames baked into a binary that may ship to a less trusted environment, and configuration changes that require a rebuild and redeploy.
Remediation Steps
Core Principle: Every security-relevant value gets exactly one definition - a named constant, enum, or policy object - that every other site references. A policy change is then one edit and cannot be applied partially. Values that additionally vary by environment move out of the build into configuration.
Locate Hard-coded Security-Relevant Constants
- Start from the literal named in the finding, then search the codebase for every other occurrence of that same value. The reported line is one occurrence; the duplicates are the weakness
- Identify which literals are security-relevant: lockout thresholds and retry limits (brute-force resistance), timeouts and token lifetimes (session exposure window), key and buffer sizes, admin account names, internal hostnames and ports
- Note which of those must also differ per environment - those need externalizing as well as naming
Give Each Value One Definition (Primary Defense)
Replace every occurrence with a reference to a single named definition, so the value exists in exactly one place:
// VULNERABLE - the same policy value written out at four call sites
if failed_attempts >= 5: lock_account() // auth/login.py
if attempts > 5: send_alert() // monitoring/alerts.py
MAX_TRIES = 5 // api/v2/session.py
"You have 5 attempts remaining" // templates/login.html
// SECURE - one definition, referenced everywhere
class SecurityPolicy:
MAX_LOGIN_ATTEMPTS = 5
if failed_attempts >= SecurityPolicy.MAX_LOGIN_ATTEMPTS: lock_account()
Tightening the threshold to 3 is now one edit that reaches every site. In the vulnerable form it is four edits, and a site that gets missed stays at the old value silently, because nothing compares the copies.
Externalize the Values That Vary by Environment
// VULNERABLE - hard-coded, environment-specific, and platform-specific
DB_HOST = '192.168.1.100'
UPLOAD_DIR = 'C:\uploads' // breaks on any non-Windows deployment
API_URL = 'http://internal-server.local:8080'
// SECURE - loaded from configuration, with a platform-independent path helper
config = load_config(config_path_for(current_environment()))
DB_HOST = config.database.host
UPLOAD_DIR = platform_data_dir('myapp') // resolves to the right location per OS
API_URL = config.api.base_url
A named constant is enough for a value that is the same everywhere - SecurityPolicy.MAX_LOGIN_ATTEMPTS above needs one definition, not a config file. Externalize only what genuinely differs between environments; a config entry for a value that never varies adds a second place for it to disagree with itself.
Use one configuration file (or config source) per environment (dev/staging/production), selected at startup based on an environment variable - not a single file with hardcoded production values that happens to also get used in dev.
Keep Secrets Out of Configuration Files Entirely
Non-secret configuration (hostnames, timeouts, feature flags) can live in a checked-in config file. Secrets (passwords, API keys, signing keys) must not: config files get committed, backed up, and copied around like any other file. Read secrets at runtime from a secrets manager, or from a file mounted with restricted permissions. An environment variable is a reasonable way for an orchestrator to hand the process a secret it fetched from such a store at start-up, but not a place to keep one - CWE-526 covers that distinction, and CWE-522 has the full guidance on protecting credentials.
Use Platform-Independent Path Resolution
Don't build paths with a hardcoded separator or drive letter assumption. Use the language's standard path-joining and standard-directory-location APIs (e.g. a Path/pathlib-style API, or a "user data directory" helper) so the same code resolves a sensible location on Windows, macOS, and Linux without a runtime OS check scattered through the codebase.
Test the Fix
- Change the value in its single definition and confirm every dependent behaviour changes with it: the lockout fires at the new threshold, the message shown to the user quotes the new number, the alert fires at the same point
- Search the codebase for the old literal and confirm no occurrence remains
- Deploy to a second environment with different configuration and verify the application picks up the new values without a code change
- Verify the application runs correctly on a different OS than the one it was written on, if it is meant to be portable
- Re-scan with the security scanner to confirm the finding is resolved
Common Vulnerable Patterns
// VULNERABLE - pseudo-code
// auth/session.py
SESSION_TIMEOUT = 1800 // the policy value, written out here
// api/middleware.py
if idle_seconds > 1800: // ... and again here
expire_session()
// templates/account.html
"Your session ends after 30 minutes" // ... and again here, in another unit
// admin/config.py
ADMIN_USER = 'administrator' // a security decision keyed on a literal name
if current_user == ADMIN_USER:
grant_admin()
Why this is vulnerable: the session timeout exists in three places and one of them is in minutes. Shortening the policy to 15 minutes means finding all three, and nothing fails if you find only two. The middleware keeps expiring at the old interval and the page keeps quoting the old number until somebody notices the mismatch in production. The ADMIN_USER check adds a second problem: it is a security decision keyed on a literal string, so it grants admin to any account holding that name rather than consulting an authorization record.
Secure Patterns
// SECURE - pseudo-code
// security/policy.py - one definition, referenced everywhere
class SecurityPolicy:
SESSION_TIMEOUT = timedelta(minutes=30)
// api/middleware.py
if idle_time > SecurityPolicy.SESSION_TIMEOUT:
expire_session()
// templates/account.html
"Your session ends after {int(SecurityPolicy.SESSION_TIMEOUT.total_seconds() // 60)} minutes"
// admin - an authorization check, not a name comparison
if current_user.has_role('ADMIN'):
grant_admin()
Why this works: the timeout has one definition, and both the enforcement and the message shown to the user read from it rather than restating the number. A policy change cannot be applied partially, because no other site holds the value. Using a timedelta rather than a bare 1800 also removes the unit ambiguity that let the template disagree with the middleware. The authorization check consults the user's actual role, so it stays correct regardless of what any particular admin account is named.