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) {