Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions server/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@
* Read the database config
* @throws {Error} If the config is invalid
* @typedef {string|undefined} envString
* @returns {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} Database config
* @returns {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString, ssl:boolean, ssl_ca:envString}} Database config
*/
static readDBConfig() {
let dbConfig;
Expand All @@ -185,13 +185,37 @@

/**
* @typedef {string|undefined} envString
* @param {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} dbConfig the database configuration that should be written
* @param {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString, ssl:boolean, ssl_ca:envString}} dbConfig the database configuration that should be written
* @returns {void}
*/
static writeDBConfig(dbConfig) {
fs.writeFileSync(path.join(Database.dataDir, "db-config.json"), JSON.stringify(dbConfig, null, 4));
}

/**
* Conditionally generates the MySQL SSL configuration object.
* @param {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString, ssl:boolean, ssl_ca:envString}} dbConfig the database configuration
Comment on lines +195 to +197
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation: The JSDoc @typedef is defined inline but should be placed outside the function parameter documentation for better readability and reusability. Additionally, the complex type definition for dbConfig is duplicated across multiple methods. Consider defining a proper typedef once at the class level or file level and reusing it.

For example:

/**
 * @typedef {Object} DBConfig
 * @property {string} type - Database type
 * @property {envString} [hostname] - Database hostname
 * @property {envString} [port] - Database port
 * @property {envString} [database] - Database name
 * @property {envString} [username] - Database username
 * @property {envString} [password] - Database password
 * @property {boolean} [ssl] - Enable SSL connection
 * @property {envString} [ssl_ca] - Path to SSL CA certificate
 */

/**
 * Conditionally generates the MySQL SSL configuration object.
 * @param {DBConfig} dbConfig the database configuration
 * @returns {{ssl: object} | undefined} An object containing the ssl configuration, or undefined.
 */

This same typedef appears to be duplicated in lines 169, 189, and 198.

Copilot uses AI. Check for mistakes.
* @returns {{ssl: object} | undefined} An object containing the ssl configuration, or undefined if SSL is not enabled.
*/
static getSslConfig(dbConfig) {
if (!dbConfig.ssl) {
return undefined;
}

const sslOptions = {};
if (dbConfig.ssl_ca) {
try {
sslOptions.ca = fs.readFileSync(dbConfig.ssl_ca, "utf8");

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
} catch (error) {
log.warn("db", `Failed to read CA file from ${dbConfig.ssl_ca}: ${error.message}`);
sslOptions.rejectUnauthorized = false;
Comment on lines +210 to +211
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security concern: When the CA file cannot be read (e.g., due to file not found or permission issues), the code silently falls back to rejectUnauthorized: false, which disables certificate verification entirely. This creates a security vulnerability as it makes the connection susceptible to man-in-the-middle attacks.

Consider either:

  1. Throwing an error when the CA file path is provided but cannot be read, forcing the user to fix the configuration
  2. At minimum, use log.error instead of log.warn to make the severity clear, since this is a critical security degradation

The current approach of silently accepting insecure connections when a CA path is misconfigured could lead to production security issues going unnoticed.

Suggested change
log.warn("db", `Failed to read CA file from ${dbConfig.ssl_ca}: ${error.message}`);
sslOptions.rejectUnauthorized = false;
log.error("db", `Failed to read CA file from ${dbConfig.ssl_ca}: ${error.message}`);
throw new Error(`Failed to read database SSL CA file from ${dbConfig.ssl_ca}: ${error.message}`);

Copilot uses AI. Check for mistakes.
}
} else {
sslOptions.rejectUnauthorized = false;
}
Comment on lines +206 to +215
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security concern: When SSL is enabled but no CA certificate path is provided, the code defaults to rejectUnauthorized: false, which disables certificate verification. This approach is insecure by default.

Consider implementing one of these safer alternatives:

  1. Default to rejectUnauthorized: true (use system CA bundle) and only set it to false if explicitly configured via a separate environment variable like UPTIME_KUMA_DB_SSL_REJECT_UNAUTHORIZED=false
  2. Require the CA certificate path when SSL is enabled, failing the connection if neither CA nor an explicit opt-out is provided

The current behavior means users enabling SSL thinking they're securing their connection are actually vulnerable to MITM attacks unless they also provide a CA certificate.

Suggested change
if (dbConfig.ssl_ca) {
try {
sslOptions.ca = fs.readFileSync(dbConfig.ssl_ca, "utf8");
} catch (error) {
log.warn("db", `Failed to read CA file from ${dbConfig.ssl_ca}: ${error.message}`);
sslOptions.rejectUnauthorized = false;
}
} else {
sslOptions.rejectUnauthorized = false;
}
// Default to secure behavior: verify certificates unless explicitly disabled.
let rejectUnauthorized = true;
const rejectEnv = process.env.UPTIME_KUMA_DB_SSL_REJECT_UNAUTHORIZED;
if (typeof rejectEnv === "string" && rejectEnv.toLowerCase() === "false") {
rejectUnauthorized = false;
}
if (dbConfig.ssl_ca) {
try {
sslOptions.ca = fs.readFileSync(dbConfig.ssl_ca, "utf8");
} catch (error) {
log.warn("db", `Failed to read CA file from ${dbConfig.ssl_ca}: ${error.message}`);
}
}
sslOptions.rejectUnauthorized = rejectUnauthorized;

Copilot uses AI. Check for mistakes.
return { ssl: sslOptions };
}

/**
* Connect to the database
* @param {boolean} testMode Should the connection be started in test mode?
Expand Down Expand Up @@ -284,6 +308,7 @@
port: dbConfig.port,
user: dbConfig.username,
password: dbConfig.password,
...Database.getSslConfig(dbConfig),
});

// Set to true, so for example "uptime.kuma", becomes `uptime.kuma`, not `uptime`.`kuma`
Expand All @@ -302,6 +327,7 @@
password: dbConfig.password,
database: dbConfig.dbName,
timezone: "Z",
...Database.getSslConfig(dbConfig),
typeCast: function (field, next) {
if (field.type === "DATETIME") {
// Do not perform timezone conversion
Expand Down
3 changes: 3 additions & 0 deletions server/setup-database.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ class SetupDatabase {
dbConfig.dbName = process.env.UPTIME_KUMA_DB_NAME;
dbConfig.username = getEnvOrFile("UPTIME_KUMA_DB_USERNAME");
dbConfig.password = getEnvOrFile("UPTIME_KUMA_DB_PASSWORD");
dbConfig.ssl = process.env.UPTIME_KUMA_DB_SSL === "1";
dbConfig.ssl_ca = process.env.UPTIME_KUMA_DB_SSL_CA;
Database.writeDBConfig(dbConfig);
}
}
Expand Down Expand Up @@ -239,6 +241,7 @@ class SetupDatabase {
user: dbConfig.username,
password: dbConfig.password,
database: dbConfig.dbName,
...Database.getSslConfig(dbConfig),
});
await connection.execute("SELECT 1");
connection.end();
Expand Down
Loading