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.
Severity
CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:NCVSS assumptions:
HOST: "localhost"; the audit did not establish the network exposure of the MySQL listener, so theconservative (local attacker) variant is reported rather than assuming remote reachability.
and modify all data the instance holds.
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 acondition, not as the reported score.
Summary
app/config/db.config.jscontains the database username and password as plaintext string literals inside theapplication source, together with the host and database name. The values are the MySQL superuser
rootand thepassword
123456, and they are consumed directly by the connection pool inapp/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
bezkoder/nodejs-express-mysqlapp/config/db.config.js(lines 1–6)app/models/db.js,createPool()(lines 4–9) —host,user,password,databaseare passed tomysql.createPool68d3959; current values introduced inbe702f0Technical Description
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
.envexclusion from version control.
Attack Preconditions
application authentication exists at all.
db.config.json disk), plusreachability to the MySQL listener to use the password. With the shipped
HOST: "localhost", that meanslocal access to the host; remote use additionally requires the database to be network-exposed.
USER=root,PASSWORD=123456so that the application's own configuration would be exercised unchanged; thehost'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.jsPoC 2 — Demonstrate the credential is functional (local, harmless)
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
GET /api/tutorials -> HTTP 200.root@localhost.{"id":1,"title":"module.exports = {\n HOST: \"localhost\",\n USER: \"root\",\n PASSWORD: \"123456\",\n DB: \"testdb\"\n};","description":"3","published":4}/tmp/opencode/postfix-verify.log, section G —db.config.jscredentialsunchanged).
(
USER: "b7e24378878xxx",PASSWORD: "0200exxxx", hostus-cdbr-iron-east-02.cleardb.net), partiallymasked with
xxx. If any such value was ever real, it must be rotated; the audit did not verify whetherthose 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 modifyall 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
(
07-overprivileged-database-account.md), which multiplies the value of the leaked secret; and theSQL-injection file-read primitive (
01-sql-injection.md) provides an additional path to read the file frominside 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
app/config/db.config.jslines 1–6 (pristine68d3959), consumed byapp/models/db.jslines 4–9.GET /api/tutorials -> HTTP 200; database client authenticated asroot@localhost.LOAD_FILE()output recorded in/tmp/opencode/audit-lab-FINAL.log(section 2e and thegeneral-log entry in section 11).
/tmp/opencode/postfix-verify.log, section G.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
Fail fast at startup when a required variable is missing (
if (!process.env.DB_PASSWORD) throw ...) so theapplication 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
07-overprivileged-database-account.md)..env*in.gitignore.Affected Versions / Git History
app/config/db.config.jswas introduced in the initial commit8260dc6(2019-09-25) with ClearDB/Herokucredential-shaped values (partially masked) and was rewritten in
be702f0(2021-11-10), which introduced thecurrent
root/123456values.master, commit68d3959(2024-02-04). No release tags exist, so theaffected version is described by commit.