Work factor configured below the accepted minimum

ID

weak_key_strength_in_config

Severity

low

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Family

Cryptography

Tags

ASVS50:v11.2.3, ASVS50:v11.4.4, CWE:326, CWE:916, configuration, cryptography, non-reachable, security, source-code

Description

The right algorithm can still be configured too weakly, and the number that decides it lives only in configuration: a bcrypt cost in config/hashing.php or in a Python settings.py (BCRYPT_ROUNDS, BCRYPT_LOG_ROUNDS), an Argon2 memory cost beside it, an HSTS max-age in Django’s SECURE_HSTS_SECONDS or a helmet configuration.

This detector reports a value below the floor the catalogue records for that key — bcrypt cost 10 (ASVS), Argon2 memory 19456 KiB (OWASP, at default parallelism), HSTS max-age one year.

Only literal numbers are judged. 'rounds' ⇒ env('BCRYPT_ROUNDS', 12) is not a literal, and is left alone rather than guessed at: inferring a value would either invent a finding or hide one. The crypto coverage map states this limit for the app-config channel as a whole.

The finding links to the algorithm the work factor qualifies, since the factor is that algorithm’s strength — a bcrypt cost of 8 concerns the bcrypt asset in the CBOM. Usually that algorithm is declared elsewhere in the same file ('driver' ⇒ 'bcrypt' beside Laravel’s rounds); where the key’s own name is the only thing that names it, the catalogue says so, and the CBOM records the algorithm that key declares.

Security

A password hash’s only defence against an offline attacker is how long it takes to compute. bcrypt at cost 8 is four times cheaper than at cost 10 and roughly sixteen times cheaper than at cost 12, and the comparison that matters is against a GPU rig, not against the application server: dropping two cost factors turns a months-long cracking campaign into a weekend. Argon2 configured below the OWASP memory floor loses the memory-hardness that is the reason to choose Argon2 at all, making it no more GPU-resistant than a plain iterated hash.

A short HSTS max-age is a different failure of the same kind. The header only protects a client that has already seen it and still remembers it, so a max-age of a few hours leaves most first requests of most days open to a downgrade.

Mitigation / Fix

Raise the value, and keep it configurable so it can be raised again as hardware improves:

// config/hashing.php
'bcrypt' => [
    'rounds' => env('BCRYPT_ROUNDS', 12),
],

'argon' => [
    'memory' => 65536,
    'threads' => 1,
    'time' => 4,
],
# Python — Flask-Bcrypt writes BCRYPT_ROUNDS, django-bcrypt BCRYPT_LOG_ROUNDS; both are log2 rounds
BCRYPT_LOG_ROUNDS = 12

# Django — one year, and only once the subdomain and redirect settings are correct
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_SSL_REDIRECT = True

Pick the cost by measuring on the hardware that will run it: aim for roughly 250 ms per hash on the production instance type, and re-measure when that instance type changes. Existing hashes keep their recorded cost, so rehash on next successful login to move the population up.

Raise SECURE_HSTS_SECONDS in steps (a few minutes, then a day, then a year) and confirm every subdomain serves TLS before including them — an HSTS header with a long max-age cannot be withdrawn from clients that already cached it.