diff --git a/src/node/utils/ExportHtml.ts b/src/node/utils/ExportHtml.ts
index 99d77262039..e356c39bdbd 100644
--- a/src/node/utils/ExportHtml.ts
+++ b/src/node/utils/ExportHtml.ts
@@ -63,7 +63,12 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
// like
hooks.aCallAll('exportHtmlAdditionalTagsWithData', pad).then((newProps: string[]) => {
newProps.forEach((prop) => {
- tags.push(`span data-${prop[0]}="${prop[1]}"`);
+ // Attribute names/values here originate from the pad's attribute pool
+ // (user content), so escape the value and constrain the data-* name
+ // before interpolating, consistent with the escaping already applied to
+ // exported URLs and text below.
+ const dataName = String(prop[0]).replace(/[^a-zA-Z0-9_-]/g, '');
+ tags.push(`span data-${dataName}="${Security.escapeHTMLAttribute(String(prop[1]))}"`);
props.push(prop);
});
}),
diff --git a/src/node/utils/Settings.ts b/src/node/utils/Settings.ts
index 33240738674..4dd34a616ba 100644
--- a/src/node/utils/Settings.ts
+++ b/src/node/utils/Settings.ts
@@ -1341,6 +1341,41 @@ export const reloadSettings = () => {
settings.cookie.prefix = '';
}
+ // Warn when an account still uses a placeholder/example password from the
+ // shipped config; these should be changed before the instance is exposed.
+ // Logged loudly (error level in production) rather than throwing, so test
+ // fixtures and existing setups that use placeholder credentials still run.
+ {
+ const weakPasswords = new Set(['changeme1', 'changeme', 'admin', 'password', '']);
+ const users = (settings.users || {}) as Record;
+ const offenders = Object.keys(users).filter((name) =>
+ users[name] && typeof users[name].password === 'string' &&
+ weakPasswords.has(users[name].password as string));
+ if (offenders.length) {
+ const msg = `Account(s) using a default/placeholder password: ${offenders.join(', ')}. ` +
+ 'Set a strong password (or use the ep_hash_auth plugin) before exposing this instance.';
+ if (process.env.NODE_ENV === 'production') logger.error(msg);
+ else logger.warn(msg);
+ }
+
+ // Same check for OIDC client secrets when SSO is configured: the shipped
+ // templates fall back to placeholder values if ADMIN_SECRET / USER_SECRET
+ // are not provided.
+ const sso = (settings as any).sso;
+ const ssoClients: Array<{client_id?: string, client_secret?: string}> =
+ (sso && Array.isArray(sso.clients)) ? sso.clients : [];
+ const weakSecrets = new Set(['admin', 'user', 'secret', 'changeme', '']);
+ const secretOffenders = ssoClients
+ .filter((c) => c && typeof c.client_secret === 'string' && weakSecrets.has(c.client_secret))
+ .map((c) => c.client_id || '(unnamed client)');
+ if (secretOffenders.length) {
+ const msg = `SSO client(s) using a default/placeholder client_secret: ${secretOffenders.join(', ')}. ` +
+ 'Set a strong secret (e.g. via the ADMIN_SECRET / USER_SECRET env vars) before enabling SSO in production.';
+ if (process.env.NODE_ENV === 'production') logger.error(msg);
+ else logger.warn(msg);
+ }
+ }
+
if (settings.dbType === 'dirty') {
const dirtyWarning = 'DirtyDB is used. This is not recommended for production.';
if (!settings.suppressErrorsInPadText) {