Skip to content

# Hardcoded Database Credentials (root / 123456) Committed to Source #20

Description

@BL4CK570RM

Severity

  • Severity: Medium (High where the database accepts connections from beyond loopback)
  • CVSS v3.1 score: 7.7 — conservative, evidence-aligned variant
  • CVSS v3.1 vector: CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
  • CWE: CWE-798 — Use of Hard-coded Credentials

CVSS assumptions:

  • AV:L is used because the configuration shipped in the repository points the application at
    HOST: "localhost"; the audit did not establish the network exposure of the MySQL listener, so the
    conservative (local attacker) variant is reported rather than assuming remote reachability.
  • C:H / I:H — the leaked credential is the database superuser, so whoever authenticates with it can read
    and modify all data the instance holds.
  • Conditional variant: if the database accepts connections over the network, the vector becomes
    CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N = 9.1. The audit did not test this, so it is stated as a
    condition, not as the reported score.

Summary

app/config/db.config.js contains the database username and password as plaintext string literals inside the
application source, together with the host and database name. The values are the MySQL superuser root and the
password 123456, and they are consumed directly by the connection pool in app/models/db.js.

This is an embedded-credential weakness in the shipped configuration: any party who obtains the source —
through repository access, a source leak, the file-read primitive demonstrated in the SQL-injection finding, or
an unprivileged local account that can read the checkout — obtains a working database password, with no secret
management, rotation or environment separation involved.

The credentials were demonstrated to be live in the local reproduction environment: the lab database was
bootstrapped with exactly these values and the application then connected and served requests successfully.
No claim is made about credentials or infrastructure belonging to the upstream project's deployment; the
finding is that a superuser password ships inside the application's default configuration.

Affected Component

Item Value
Repository bezkoder/nodejs-express-mysql
Affected file app/config/db.config.js (lines 1–6)
Consumed by app/models/db.js, createPool() (lines 4–9) — host, user, password, database are passed to mysql.createPool
Affected endpoints none directly; every database operation runs under this configuration
Affected commit 68d3959; current values introduced in be702f0

Technical Description

// app/config/db.config.js (pristine 68d3959, lines 1-6)
module.exports = {
  HOST: "localhost",
  USER: "root",
  PASSWORD: "123456",
  DB: "testdb"
};
// app/models/db.js
var connection = mysql.createPool({
  host: dbConfig.HOST,
  user: dbConfig.USER,
  password: dbConfig.PASSWORD,
  database: dbConfig.DB
});

The secret is a compile-time constant in tracked source. There is no indirection (environment variable,
secret store, externalized config), no distinction between development and production values, and no .env
exclusion from version control.

Attack Preconditions

  • Authentication required: not applicable at the application layer; for using the leaked credential, no
    application authentication exists at all.
  • Network access required to exploit: read access to the source (or to db.config.js on disk), plus
    reachability to the MySQL listener to use the password. With the shipped HOST: "localhost", that means
    local access to the host; remote use additionally requires the database to be network-exposed.
  • Privileges required: none at the application layer.
  • Special configuration required: none — the values are the default configuration.
  • Works against a default installation: yes — the credential file is part of the default checkout.
  • Assumptions used in the local reproduction: an isolated throwaway MariaDB datadir was initialised with
    USER=root, PASSWORD=123456 so that the application's own configuration would be exercised unchanged; the
    host's system MariaDB was not touched.

Step-by-Step Reproduction / PoC

PoC 1 — Read the credentials from the shipped configuration

sed -n '1,6p' app/config/db.config.js
module.exports = {
  HOST: "localhost",
  USER: "root",
  PASSWORD: "123456",
  DB: "testdb"
};

PoC 2 — Demonstrate the credential is functional (local, harmless)

# start the application exactly as configured, then confirm it authenticated
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/api/tutorials
# => 200

# the same values authenticate a database client (as the audit harness did)
export MYSQL_PWD=123456
mysql --socket=/path/to/mysqld.sock -uroot -e "SELECT CURRENT_USER();"

PoC 3 — Show the secret is reachable through the application's own file-read primitive

Demonstrated in the SQL-injection evidence: a LOAD_FILE() of the configuration file returned its contents
(see 01-sql-injection.md, PoC/observed result).

Observed Result

  • The application, configured with exactly these values, served requests successfully:
    GET /api/tutorials -> HTTP 200.
  • The database client authenticated with the same values and reported the account as root@localhost.
  • The file-read primitive returned the file verbatim:
{"id":1,"title":"module.exports = {\n  HOST: \"localhost\",\n  USER: \"root\",\n  PASSWORD: \"123456\",\n  DB: \"testdb\"\n};","description":"3","published":4}
  • Still present after the code fix (/tmp/opencode/postfix-verify.log, section G — db.config.js credentials
    unchanged).
  • Git history also contains credential-shaped ClearDB/Heroku values in the 2019 revision of the same file
    (USER: "b7e24378878xxx", PASSWORD: "0200exxxx", host us-cdbr-iron-east-02.cleardb.net), partially
    masked with xxx. If any such value was ever real, it must be rotated; the audit did not verify whether
    those historical values were ever valid.

Expected Secure Result

  • No credential of any kind appears in tracked source or in the repository history.

  • Credentials are supplied per environment through environment variables or a secret manager, with no
    development default secret.

  • .env/local configuration files are excluded from version control, and secrets are rotated on exposure.

Security Impact

Direct impact

Anyone who obtains the source obtains a working database password for the configured account. Because that
account is root, compromise of the secret means complete control of the database instance: read and modify
all data, create or drop accounts, and (where the server permits it) read and write files. On a shared host,
the leaked local credential also elevates any local user who can reach the database socket/TCP port to
superuser.

Amplified impact

  • Root cause of this finding: plaintext credential constants in tracked source (this file).
  • Amplifiers (separate findings, not causes): the account is a superuser
    (07-overprivileged-database-account.md), which multiplies the value of the leaked secret; and the
    SQL-injection file-read primitive (01-sql-injection.md) provides an additional path to read the file from
    inside the application. Removing the hardcoded secret does not fix either of those, and fixing them does not
    remove the need to externalize the secret.

Evidence

  • Source: app/config/db.config.js lines 1–6 (pristine 68d3959), consumed by app/models/db.js lines 4–9.
  • Functional-credential proof: application started with these values returned
    GET /api/tutorials -> HTTP 200; database client authenticated as root@localhost.
  • File-read proof: LOAD_FILE() output recorded in /tmp/opencode/audit-lab-FINAL.log (section 2e and the
    general-log entry in section 11).
  • Post-fix status: /tmp/opencode/postfix-verify.log, section G.
  • Git history: git log --oneline -- app/config/db.config.js → be702f0, 8260dc6.

Root Cause

The application configuration model treats the database password as a source constant rather than as secret
input, so a superuser credential is versioned alongside the code it configures.

Remediation

// app/config/db.config.js
module.exports = {
  HOST: process.env.DB_HOST || "localhost",
  USER: process.env.DB_USER,          // no default, no value in source
  PASSWORD: process.env.DB_PASSWORD,  // no default, no value in source
  DB: process.env.DB_NAME || "testdb"
};
# .env (untracked) — and add ".env" to .gitignore
DB_HOST=localhost
DB_USER=app_user
DB_PASSWORD=<long-random-generated-secret>
DB_NAME=testdb

Fail fast at startup when a required variable is missing (if (!process.env.DB_PASSWORD) throw ...) so the
application never silently falls back to a default. Rotate any credential that has ever been committed, and
scan the repository history for other leaked secrets.

Defense in Depth

  • Use a non-superuser account (see 07-overprivileged-database-account.md).
  • Store secrets in a secrets manager for anything beyond local development.
  • Add secret scanning (gitleaks/trufflehog) to CI, and include .env* in .gitignore.
  • Restrict filesystem permissions on the checkout and separate development from production configuration.

Affected Versions / Git History

  • app/config/db.config.js was introduced in the initial commit 8260dc6 (2019-09-25) with ClearDB/Heroku
    credential-shaped values (partially masked) and was rewritten in be702f0 (2021-11-10), which introduced the
    current root / 123456 values.
  • Those values are present in current master, commit 68d3959 (2024-02-04). No release tags exist, so the
    affected version is described by commit.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions