diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index c37da0a69..0f3de33c3 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -28,6 +28,7 @@ const settings = require('../utils/Settings'); const webaccess = require('../hooks/express/webaccess'); const log4js = require('log4js'); const authLogger = log4js.getLogger('auth'); +const {padutils} = require('../../static/js/pad_utils'); const DENY = Object.freeze({accessStatus: 'deny'}); @@ -106,6 +107,11 @@ exports.checkAccess = async (padID, sessionCookie, token, userSettings) => { authLogger.debug('access denied: HTTP API session is required'); return DENY; } + if (!sessionAuthorID && token != null && !padutils.isValidAuthorToken(token)) { + // The author token should be kept secret, so do not log it. + authLogger.debug('access denied: invalid author token'); + return DENY; + } const grant = { accessStatus: 'grant', diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 07fffa6d2..c37920ead 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -177,7 +177,7 @@ const sendClientReady = (isReconnect) => { } let token = Cookies.get('token'); - if (token == null) { + if (token == null || !padutils.isValidAuthorToken(token)) { token = padutils.generateAuthorToken(); Cookies.set('token', token, {expires: 60}); } diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js index ac45d9ca3..e10841f50 100644 --- a/src/static/js/pad_utils.js +++ b/src/static/js/pad_utils.js @@ -88,6 +88,9 @@ const urlRegex = (() => { `(?:${withAuth}|${withoutAuth}|www\\.)${urlChar}*(?!${postUrlPunct})${urlChar}`, 'g'); })(); +// https://stackoverflow.com/a/68957976 +const base64url = /^(?=(?:.{4})*$)[A-Za-z0-9_-]*(?:[AQgw]==|[AEIMQUYcgkosw048]=)?$/; + const padutils = { /** * Prints a warning message followed by a stack trace (to make it easier to figure out what code @@ -328,6 +331,21 @@ const padutils = { } }), + /** + * Returns whether a string has the expected format to be used as a secret token identifying an + * author. The format is defined as: 't.' followed by a non-empty base64url string (RFC 4648 + * section 5 with padding). + * + * Being strict about what constitutes a valid token enables unambiguous extensibility (e.g., + * conditional transformation of a token to a database key in a way that does not allow a + * malicious user to impersonate another user). + */ + isValidAuthorToken: (t) => { + if (typeof t !== 'string' || !t.startsWith('t.')) return false; + const v = t.slice(2); + return v.length > 0 && base64url.test(v); + }, + /** * Returns a string that can be used in the `token` cookie as a secret that authenticates a * particular author.