How Hardcoded Default Credentials Happen in Node.js Database Initialization and How to Fix It
Introduction
The hubcmdui/database/database.js file is responsible for bootstrapping the application's SQLite database, including creating the initial administrator account the first time the application runs. That sounds routine — and it is — but a single logger.info call on line 228 turned a standard initialization task into a critical security exposure. The log message printed the default username and password in plaintext:
logger.info('默认管理员用户创建成功: root/admin@123');
Combined with the fact that the username root and password admin@123 are hardcoded in the source code itself, this meant that every operator who deployed the application without immediately changing the default password was running a system that any attacker — armed with nothing more than a GitHub search — could log into as a full administrator.
This post walks through exactly how the vulnerability works, what the fix does, and how to avoid the same pattern in your own Node.js projects.
The Vulnerability Explained
What the Code Does
During database initialization, the application checks whether an admin user already exists. If none is found, it runs an INSERT statement to create one:
// hubcmdui/database/database.js — BEFORE the fix (line ~232)
db.run(
'INSERT INTO users (username, password, created_at, login_count, last_login) VALUES (?, ?, ?, ?, ?)',
['root', hashedPassword, new Date().toISOString(), 0, null]
);
logger.info('默认管理员用户创建成功: root/admin@123');
Two problems exist here, and they compound each other:
-
The plaintext password is embedded in source code. The string
admin@123lives in the repository. Anyone who reads the code — including anyone who finds the project on GitHub, a code mirror, or a leaked backup — now knows the default password. -
The plaintext password is written to the application log. Even if the source code were private, the log line
root/admin@123would appear in every log file, log aggregator, and SIEM that ingests the application's output. Log files are frequently stored with weaker access controls than the application itself, forwarded to third-party services, and included in support bundles.
The Exploitation Path
The exploitation scenario is two steps and requires no special tooling:
Step 1 — Discover the credentials. An attacker searches GitHub (or any code-hosting platform) for the string admin@123 combined with the project name. The plaintext log message confirms both the username and the password in a single line. Alternatively, the attacker reads a log file obtained through an unrelated vulnerability (e.g., a path traversal or misconfigured log endpoint).
Step 2 — Authenticate. The attacker sends a POST request to /api/login:
POST /api/login HTTP/1.1
Content-Type: application/json
{
"username": "root",
"password": "admin@123"
}
Because the application created the root account with hashedPassword derived from admin@123, this request succeeds on every deployment where the operator has not manually changed the password. The attacker now has full administrative access.
Why This Is Classified as Critical
The vulnerability is rated critical for three reasons:
- No attacker skill required. The credentials are public. Exploitation is a copy-paste operation.
- Every default deployment is affected. The vulnerable code runs on first startup. Any operator who did not change the password immediately after deployment is exposed.
- Full administrative compromise. The
rootuser has unrestricted access to all application functionality, making lateral movement within the application trivial.
This maps to CWE-798: Use of Hard-coded Credentials and aligns with OWASP A07:2021 — Identification and Authentication Failures.
The Fix
The pull request makes a targeted, surgical change to the log message on line 228:
Before
logger.info('默认管理员用户创建成功: root/admin@123');
After
logger.info('默认管理员用户创建成功,请及时修改默认密码');
Translation: The new message reads "Default admin user created successfully. Please change the default password promptly."
Why This Change Is Sufficient (and What It Does Not Change)
The fix removes the information-disclosure path. The INSERT statement itself is unchanged — the application still creates the root account with hashedPassword on first run. The difference is that the log no longer broadcasts the plaintext password to every system that consumes the application's log output.
The fix also shifts the message from confirmation to action: instead of telling operators "here are your credentials," it tells them "you need to change your credentials." This is a small UX nudge, but it matters operationally — operators who see the new message are more likely to act on it.
What a Deeper Fix Would Look Like
The PR addresses the immediate information-disclosure risk. A more comprehensive remediation would include:
-
Force a password change on first login. Set a
must_change_passwordflag in theuserstable and redirect therootuser to a password-change screen before granting access to any other functionality. -
Generate a random initial password. Instead of hardcoding
admin@123, generate a cryptographically random password at first run, print it once to stdout (not to a persistent log), and require the operator to record it:
const crypto = require('crypto');
const initialPassword = crypto.randomBytes(16).toString('hex');
const hashedPassword = await bcrypt.hash(initialPassword, 12);
db.run(
'INSERT INTO users (username, password, created_at, login_count, last_login) VALUES (?, ?, ?, ?, ?)',
['root', hashedPassword, new Date().toISOString(), 0, null]
);
// Print ONCE to stdout, never to a persistent log
process.stdout.write(`\n[SETUP] Initial admin password: ${initialPassword}\nChange this immediately.\n\n`);
- Accept credentials via environment variables. Allow operators to set
ADMIN_USERNAMEandADMIN_PASSWORDbefore first run, so the initial account is created with operator-chosen credentials rather than a default.
Prevention & Best Practices
Never Log Credential Values
Treat any string that contains a password, token, or secret as toxic to your logging pipeline. Use structured logging and explicitly exclude sensitive fields:
// BAD — logs the secret
logger.info(`User created: ${username}/${password}`);
// GOOD — logs only non-sensitive metadata
logger.info('Admin user initialized', { username, action: 'first_run_setup' });
Use Environment Variables or Secret Management
Credentials that must exist at startup should come from the environment, not from source code:
const adminPassword = process.env.ADMIN_PASSWORD;
if (!adminPassword) {
throw new Error('ADMIN_PASSWORD environment variable is required');
}
For production deployments, use a secrets manager (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault) rather than environment variables in .env files, which are frequently committed to version control by accident.
Scan for Secrets in CI/CD
Add secret-scanning tools to your pipeline so that hardcoded credentials are caught before they reach the main branch:
- Gitleaks — scans git history for secrets
- Semgrep — rule-based static analysis including hardcoded credential patterns
- truffleHog — entropy-based and pattern-based secret detection
Enforce Password Change on First Login
Any application that ships with a default credential — even a randomly generated one — should enforce a password change before the account becomes fully functional. This is a defense-in-depth measure that limits the window of exposure.
Security Standards
- OWASP A07:2021 — Identification and Authentication Failures: https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/
- OWASP Authentication Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- CWE-798: https://cwe.mitre.org/data/definitions/798.html
- CWE-259 (Use of Hard-coded Password): https://cwe.mitre.org/data/definitions/259.html
Key Takeaways
- The
logger.infocall indatabase.jswas the direct disclosure vector. The password was hashed in the database, but the log message printed the plaintext value on every fresh deployment — a distinction that is easy to overlook during code review. - Hardcoded credentials in initialization code are especially dangerous because they affect every deployment by default, not just misconfigured ones.
- The string
admin@123in source code is permanently public once it has been pushed to any repository, even a private one, because it may have been cloned, forked, or indexed before deletion. - Removing the credential from the log is necessary but not sufficient — a complete fix also randomizes the initial password and forces a change on first login.
- Static analysis tools can catch this pattern automatically. The Orbis AppSec scanner flagged the
logger.infocall containing credential-like strings adjacent to anINSERT INTO usersstatement, demonstrating that this class of vulnerability is reliably detectable before it reaches production.
How Orbis AppSec Detected This
- Source: The hardcoded string literal
'admin@123'embedded in the database initialization routine inhubcmdui/database/database.js - Sink:
logger.info('默认管理员用户创建成功: root/admin@123')atdatabase.js:228, which writes the plaintext credential to the application log - Missing control: No secret-scanning gate in CI/CD; no requirement to supply credentials via environment variables; no enforcement of password change on first login
- CWE: CWE-798 — Use of Hard-coded Credentials
- Fix: The
logger.infocall was updated to remove the plaintext credential and replaced with a prompt instructing operators to change the default password
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
Hardcoded default credentials are one of the oldest and most reliably exploited vulnerability classes in application security, yet they continue to appear in production codebases because they feel harmless during development — the password is "just for testing," or "operators will change it." They rarely do, and the consequences are immediate full administrative compromise for anyone who reads the source code.
The fix in database.js eliminates the plaintext credential from the log output, which closes the most direct information-disclosure path. Developers working on similar initialization routines should go further: generate random initial passwords, deliver them securely to operators exactly once, and enforce a password change before the account can be used. A single logger.info call should never be the reason an attacker gains admin access.
References
- CWE-798: Use of Hard-coded Credentials
- CWE-259: Use of Hard-coded Password
- OWASP Authentication Cheat Sheet
- OWASP A07:2021 — Identification and Authentication Failures
- Node.js Crypto Module — randomBytes
- Semgrep rules: hardcoded-credentials
- fix: the database initialization code creates a defa... in database.js