From 3ba58f754dd6099e4a885d5414c2d5d00096a7a3 Mon Sep 17 00:00:00 2001 From: Felix Eckhofer Date: Tue, 20 Jan 2026 22:40:00 +0100 Subject: [PATCH 1/5] fix: apply theme style to embedded diagram svg (#7903) * fix: Apply theme style to embedded diagram (#7903) When diagrams are created with "adaptive colors" they automatically change to light-/darkmode depending on the browser preference. This leads to rendering problems when the settings in the browser differ from the wiki.js theme setting. Fixes #7677 * fix: Update color scheme for SVG in diagram class Refactor SVG color scheme handling in diagram class. * fix: remove duplicate svg first-child class Removed unnecessary direction property from first SVG in diagram. --------- Co-authored-by: Nicolas Giard --- client/themes/default/scss/app.scss | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/client/themes/default/scss/app.scss b/client/themes/default/scss/app.scss index ef18b535..de822dc4 100644 --- a/client/themes/default/scss/app.scss +++ b/client/themes/default/scss/app.scss @@ -764,8 +764,17 @@ .diagram { margin-top: 1rem; overflow: auto; - svg:first-child { - direction: ltr; + + svg { + color-scheme: light; + + &:first-child { + direction: ltr; + } + + @at-root .theme--dark & { + color-scheme: dark !important; + } } } From 3dcf20ab6f59e72271fd84271884893c754b6d26 Mon Sep 17 00:00:00 2001 From: Felix Eckhofer Date: Thu, 22 Jan 2026 18:27:29 +0100 Subject: [PATCH 2/5] fix: diagram svg styling add `!important` qualifier for light scheme (#7905) This is required, because the element has an inline style with `color-scheme: light dark`, which has higher specificity than our css rule. --- client/themes/default/scss/app.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/themes/default/scss/app.scss b/client/themes/default/scss/app.scss index de822dc4..88380963 100644 --- a/client/themes/default/scss/app.scss +++ b/client/themes/default/scss/app.scss @@ -766,7 +766,7 @@ overflow: auto; svg { - color-scheme: light; + color-scheme: light !important; &:first-child { direction: ltr; From 6ae53bf1bd9174c97585e1bf46bfbdc86673a859 Mon Sep 17 00:00:00 2001 From: mod242 <40213799+mod242@users.noreply.github.com> Date: Sat, 24 Jan 2026 00:36:52 +0100 Subject: [PATCH 3/5] feat: map OIDC/OAuth2 avatar claims to user pictureUrl (#7908) --- .../authentication/oauth2/authentication.js | 4 +++- .../authentication/oauth2/definition.yml | 19 +++++++++++++------ .../authentication/oidc/authentication.js | 4 +++- .../authentication/oidc/definition.yml | 15 +++++++++++---- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/server/modules/authentication/oauth2/authentication.js b/server/modules/authentication/oauth2/authentication.js index ce66c3db..6ac3e830 100644 --- a/server/modules/authentication/oauth2/authentication.js +++ b/server/modules/authentication/oauth2/authentication.js @@ -22,13 +22,15 @@ module.exports = { state: conf.enableCSRFProtection }, async (req, accessToken, refreshToken, profile, cb) => { try { + const picture = _.get(profile, conf.pictureClaim, '') const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile: { ...profile, id: _.get(profile, conf.userIdClaim), displayName: _.get(profile, conf.displayNameClaim, '???'), - email: _.get(profile, conf.emailClaim) + email: _.get(profile, conf.emailClaim), + picture: picture } }) if (conf.mapGroups) { diff --git a/server/modules/authentication/oauth2/definition.yml b/server/modules/authentication/oauth2/definition.yml index 0e599629..254bec0d 100644 --- a/server/modules/authentication/oauth2/definition.yml +++ b/server/modules/authentication/oauth2/definition.yml @@ -54,38 +54,45 @@ props: default: email maxWidth: 500 order: 8 + pictureClaim: + type: String + title: Picture Claim + hint: Field containing the user avatar URL + default: picture + maxWidth: 500 + order: 9 mapGroups: type: Boolean title: Map Groups hint: Map groups matching names from the groups claim value default: false - order: 9 + order: 10 groupsClaim: type: String title: Groups Claim hint: Field containing the group names default: groups maxWidth: 500 - order: 10 + order: 11 logoutURL: type: String title: Logout URL hint: (optional) Logout URL on the OAuth2 provider where the user will be redirected to complete the logout process. - order: 11 + order: 12 scope: type: String title: Scope hint: (optional) Application Client permission scopes. - order: 12 + order: 13 useQueryStringForAccessToken: type: Boolean default: false title: Pass access token via GET query string to User Info Endpoint hint: (optional) Pass the access token in an `access_token` parameter attached to the GET query string of the User Info Endpoint URL. Otherwise the access token will be passed in the Authorization header. - order: 13 + order: 14 enableCSRFProtection: type: Boolean default: true title: Enable CSRF protection hint: Pass a nonce state parameter during authentication to protect against CSRF attacks. - order: 14 + order: 15 diff --git a/server/modules/authentication/oidc/authentication.js b/server/modules/authentication/oidc/authentication.js index 4c7383e4..bfda8c2f 100644 --- a/server/modules/authentication/oidc/authentication.js +++ b/server/modules/authentication/oidc/authentication.js @@ -24,6 +24,7 @@ module.exports = { acrValues: conf.acrValues }, async (req, iss, uiProfile, idProfile, context, idToken, accessToken, refreshToken, params, cb) => { const profile = Object.assign({}, idProfile, uiProfile) + const picture = _.get(profile, '_json.' + conf.pictureClaim, '') try { const user = await WIKI.models.users.processProfile({ @@ -31,7 +32,8 @@ module.exports = { profile: { ...profile, email: _.get(profile, '_json.' + conf.emailClaim), - displayName: _.get(profile, '_json.' + conf.displayNameClaim, '') + displayName: _.get(profile, '_json.' + conf.displayNameClaim, ''), + picture: picture } }) if (conf.mapGroups) { diff --git a/server/modules/authentication/oidc/definition.yml b/server/modules/authentication/oidc/definition.yml index 774575c1..266ea7e9 100644 --- a/server/modules/authentication/oidc/definition.yml +++ b/server/modules/authentication/oidc/definition.yml @@ -62,26 +62,33 @@ props: default: displayName maxWidth: 500 order: 9 + pictureClaim: + type: String + title: Picture Claim + hint: Field containing the user avatar URL + default: picture + maxWidth: 500 + order: 10 mapGroups: type: Boolean title: Map Groups hint: Map groups matching names from the groups claim value default: false - order: 10 + order: 11 groupsClaim: type: String title: Groups Claim hint: Field containing the group names default: groups maxWidth: 500 - order: 11 + order: 12 logoutURL: type: String title: Logout URL hint: (optional) Logout URL on the OAuth2 provider where the user will be redirected to complete the logout process. - order: 12 + order: 13 acrValues: type: String title: ACR Values hint: (optional) Authentication Context Class Reference - order: 13 + order: 14 From 7ae6635d1686afa193bd1417abd9c9891aa0b71a Mon Sep 17 00:00:00 2001 From: "Kolega.dev" Date: Thu, 12 Feb 2026 02:27:36 +0000 Subject: [PATCH 4/5] fix: validate loginRedirect cookie to prevent open redirect (#7923) The loginRedirect cookie value was used directly in res.redirect() and window.location.replace() without validation, allowing redirection to arbitrary external URLs. Added validation to ensure the redirect target is a relative path before use. Co-authored-by: kolega.dev --- client/components/login.vue | 14 ++++++++++---- server/controllers/auth.js | 14 ++++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/client/components/login.vue b/client/components/login.vue index bf9b26c2..0bbaa2a5 100644 --- a/client/components/login.vue +++ b/client/components/login.vue @@ -644,16 +644,22 @@ export default { Cookies.set('jwt', respObj.jwt, { expires: 365, secure: window.location.protocol === 'https:' }) _.delay(() => { const loginRedirect = Cookies.get('loginRedirect') + const isValidRedirect = loginRedirect && loginRedirect.startsWith('/') && !loginRedirect.startsWith('//') && !loginRedirect.includes('://') if (loginRedirect === '/' && respObj.redirect) { Cookies.remove('loginRedirect') window.location.replace(respObj.redirect) - } else if (loginRedirect) { + } else if (isValidRedirect) { Cookies.remove('loginRedirect') window.location.replace(loginRedirect) - } else if (respObj.redirect) { - window.location.replace(respObj.redirect) } else { - window.location.replace('/') + if (loginRedirect) { + Cookies.remove('loginRedirect') + } + if (respObj.redirect) { + window.location.replace(respObj.redirect) + } else { + window.location.replace('/') + } } }, 1000) } diff --git a/server/controllers/auth.js b/server/controllers/auth.js index 7a947338..733486be 100644 --- a/server/controllers/auth.js +++ b/server/controllers/auth.js @@ -73,16 +73,22 @@ router.all('/login/:strategy/callback', async (req, res, next) => { res.cookie('jwt', authResult.jwt, commonHelper.getCookieOpts()) const loginRedirect = req.cookies['loginRedirect'] + const isValidRedirect = loginRedirect && loginRedirect.startsWith('/') && !loginRedirect.startsWith('//') && !loginRedirect.includes('://') if (loginRedirect === '/' && authResult.redirect) { res.clearCookie('loginRedirect') res.redirect(authResult.redirect) - } else if (loginRedirect) { + } else if (isValidRedirect) { res.clearCookie('loginRedirect') res.redirect(loginRedirect) - } else if (authResult.redirect) { - res.redirect(authResult.redirect) } else { - res.redirect('/') + if (loginRedirect) { + res.clearCookie('loginRedirect') + } + if (authResult.redirect) { + res.redirect(authResult.redirect) + } else { + res.redirect('/') + } } } catch (err) { next(err) From d14b0a5509f1592e962b6d87324423eeb3903b1b Mon Sep 17 00:00:00 2001 From: "Kolega.dev" Date: Thu, 12 Feb 2026 02:40:25 +0000 Subject: [PATCH 5/5] fix: authenticate GraphQL subscription WebSocket connections (#7922) The onConnect handler for GraphQL subscriptions was empty, allowing any client to establish a WebSocket connection and subscribe to loggingLiveTrail without authentication. Added JWT verification in onConnect using the same RS256 credentials and permission checks (manage:system) used elsewhere. Co-authored-by: kolega.dev --- client/client-app.js | 6 +++++- server/core/servers.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/client/client-app.js b/client/client-app.js index 0d2018d7..cdf27a80 100644 --- a/client/client-app.js +++ b/client/client-app.js @@ -114,7 +114,11 @@ const graphQLWSLink = new WebSocketLink({ uri: graphQLWSEndpoint, options: { reconnect: true, - lazy: true + lazy: true, + connectionParams: () => { + const token = Cookies.get('jwt') + return token ? { token } : {} + } } }) diff --git a/server/core/servers.js b/server/core/servers.js index 95046e65..d2b2b79f 100644 --- a/server/core/servers.js +++ b/server/core/servers.js @@ -4,6 +4,8 @@ const https = require('https') const { ApolloServer } = require('apollo-server-express') const Promise = require('bluebird') const _ = require('lodash') +const jwt = require('jsonwebtoken') +const cookie = require('cookie') /* global WIKI */ @@ -125,7 +127,35 @@ module.exports = { context: ({ req, res }) => ({ req, res }), subscriptions: { onConnect: (connectionParams, webSocket) => { + let token = _.get(connectionParams, 'token', null) + if (!token) { + const cookieHeader = _.get(webSocket, 'upgradeReq.headers.cookie', '') + if (cookieHeader) { + const cookies = cookie.parse(cookieHeader) + token = cookies.jwt || null + } + } + + if (!token) { + throw new Error('Unauthorized') + } + + try { + const user = jwt.verify(token, WIKI.config.certs.public, { + audience: WIKI.config.auth.audience, + issuer: 'urn:wiki.js', + algorithms: ['RS256'] + }) + + if (!_.includes(user.permissions, 'manage:system')) { + throw new Error('Forbidden') + } + + return { user } + } catch (err) { + throw new Error('Unauthorized') + } }, path: '/graphql-subscriptions' }