Back to Blog
critical SEVERITY8 min read

How hardcoded default credentials happen in Node.js database initialization and how to fix it

A critical vulnerability was discovered in `hubcmdui/database/database.js` where the database initialization routine hardcoded the default admin credentials (`root` / `admin@123`) and logged them in plaintext. Because these credentials are visible in the public source code, any attacker who finds the repository can immediately authenticate as an administrator on any unpatched deployment. The fix removes the plaintext credential from the log message, and operators are now prompted to change the d

O
By Orbis AppSec
Published July 23, 2026Reviewed July 23, 2026

Answer Summary

This is a hardcoded default credentials vulnerability (CWE-798) in a Node.js application, specifically in `hubcmdui/database/database.js` at line 228. The initialization routine inserted a default admin user with username `root` and password `admin@123` and then logged those credentials in plaintext via `logger.info`. The fix removes the credential values from the log output and replaces them with a prompt to change the default password, eliminating the information-disclosure path while preserving the initialization logic.

Vulnerability at a Glance

cweCWE-798
fixRemove the plaintext credential from the logger.info call and replace it with a generic prompt to change the default password
riskAny attacker with access to the source code can authenticate as the root administrator on every unpatched deployment
languageJavaScript (Node.js)
root causeThe database initialization function logs the plaintext default password after creating the admin user
vulnerabilityHardcoded Default Credentials with Plaintext Logging

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:

  1. The plaintext password is embedded in source code. The string admin@123 lives 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.

  2. The plaintext password is written to the application log. Even if the source code were private, the log line root/admin@123 would 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 root user 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:

  1. Force a password change on first login. Set a must_change_password flag in the users table and redirect the root user to a password-change screen before granting access to any other functionality.

  2. 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`);
  1. Accept credentials via environment variables. Allow operators to set ADMIN_USERNAME and ADMIN_PASSWORD before first run, so the initial account is created with operator-chosen credentials rather than a default.

Key Takeaways

  • The logger.info call in database.js was 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@123 in 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.info call containing credential-like strings adjacent to an INSERT INTO users statement, 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 in hubcmdui/database/database.js
  • Sink: logger.info('默认管理员用户创建成功: root/admin@123') at database.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.info call 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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #90

Related Articles

high

TrackOptionsManager DDL: Template-Literal SQL Injection Closed

The `TrackOptionsManager` service built its `CREATE TABLE` and `ALTER TABLE ... ADD COLUMN alias` statements by interpolating a `DEFAULT_ALIAS` constant directly inside single quotes in a JavaScript template literal, and its private `_query()` helper had no parameter channel at all. The fix routes the default value through `mysql.escape()` and gives `_query(q, params = [])` a real bound-parameter argument that is forwarded to `db.query()`. This removes an injection primitive on a schema-bootstra

critical

eval() in Async Function Constructor Enables Runtime Escape

The eval.mjs command handler used raw `eval()` to execute JavaScript expressions, creating a critical code injection path if owner credentials are compromised. The fix replaces `eval()` with the `AsyncFunction` constructor and explicitly shadows `process`, `require`, and other runtime globals as parameters, preventing evaluated code from reaching the Node.js runtime even when authentication boundaries fail.

high

How SQL injection via template literals happens in Node.js SQLite and how to fix it

A SQL injection vulnerability in `src/lib/codex-state.mjs` allowed dynamic column names to reach SQL queries through JavaScript template literals. The fix implements defense-in-depth with strict identifier validation using `SAFE_IDENTIFIER` regex before query construction.

high

How Denial of Service via Crafted ZIP File happens in Node.js and how to fix it

CVE-2026-39244 is a high-severity denial of service vulnerability in the adm-zip npm package that allows attackers to crash Node.js applications by uploading maliciously crafted ZIP files. The fix upgrades adm-zip from version 0.5.16 to 0.6.0, which adds proper memory bounds checking to prevent excessive allocation during archive extraction.

critical

How origin validation bypass happens in Express.js and how to fix it

A `POST /changeData` route in `src/main/server/routes/index.js` guarded state-changing writes with an origin allowlist, but the guard was wrapped in an `if (origin && ...)` truthiness check. Any request that simply omitted both `Origin` and `Referer` — a one-line `curl` command, a local script, a background process — skipped validation entirely and modified application data. The fix removes the truthiness short-circuit so a *missing* header is now treated as a rejection, not a pass.

critical

How CSRF vulnerabilities happen in Node.js API clients and how to fix them

A critical CSRF vulnerability in `lib/client.js` allowed attackers to forge authenticated POST requests to `/remote-ssh/api/*` endpoints. The fix adds the `X-Requested-With: XMLHttpRequest` header to enable proper CSRF token validation, blocking malicious cross-site requests with a minimal one-line change.