Fix GHSA-cv95-8h7c-2ffq: Missing authorization on OIDC Meteor methods allows privilege escalation to admin (CWE-269, CWE-862).

Six Meteor methods used internally by the OIDC login flow were registered as globally DDP-callable
with no authorization, while their non-OIDC counterparts require admin. `setCreateOrgFromOidc` and
`setOrgAllFieldsFromOidc` (`server/models/org.js`), `setCreateTeamFromOidc` and
`setTeamAllFieldsFromOidc` (`server/models/team.js`) let any authenticated user create/rename/
deactivate/modify arbitrary organizations and teams — including `orgAutoAddUsersWithDomainName` —
bypassing the admin-only restriction. Most critically, `groupRoutineOnLogin`
(`packages/wekan-oidc/oidc_server.js`) sets `isAdmin` from caller-supplied group data, so with
`PROPAGATE_OIDC_DATA` enabled any authenticated user could call it over DDP with
`{groups:[{isAdmin:true,forceCreate:true}]}` and promote themselves to global admin;
`boardRoutineOnLogin` could likewise add the caller to the default board. These six methods are only
ever invoked server-side (via `Meteor.callAsync`) during the OIDC handshake, where a fix that checks
`isAdmin`/`this.userId` would break legitimate group/admin propagation (the user is not yet logged in
or admin at that point). Fixed by rejecting any direct client/DDP invocation: a server-to-server
`Meteor.callAsync` runs with `this.connection === null`, whereas a client call has a non-null
connection, so each of the six methods now throws `not-authorized` when `this.connection !== null`.
The legitimate OIDC login flow is unaffected.

Thanks to alexwaira for the coordinated disclosure, and Claude.
This commit is contained in:
Lauri Ojansivu committed 2026-05-31 00:11:18 +03:00
1 parent d475214224
commit 305864f0c7
3 files changed
+59

No files matched your search

+19
View File
@@ -336,6 +336,17 @@ var getTokenContent = function (token) {
Meteor.methods({
'groupRoutineOnLogin': async function(info, userId)
{
// SECURITY (GHSA-cv95-8h7c-2ffq): This method is invoked only server-side
// during the OIDC login flow (via Meteor.callAsync from the
// OAuth.registerService('oidc') callback above). With PROPAGATE_OIDC_DATA
// enabled it grants isAdmin based on the caller-supplied group data, so if
// it were callable directly over DDP any authenticated user could promote
// themselves to global admin. A server-side method call has
// this.connection === null; a direct client call has a non-null
// connection. Reject the latter.
if (this.connection !== null) {
throw new Meteor.Error('not-authorized');
}
check(info, Object);
check(userId, String);
var propagateOidcData = process.env.PROPAGATE_OIDC_DATA || false;
@@ -360,6 +371,14 @@ Meteor.methods({
Meteor.methods({
'boardRoutineOnLogin': async function(info, oidcUserId)
{
// SECURITY (GHSA-cv95-8h7c-2ffq): Invoked only server-side during the OIDC
// login flow (via Meteor.callAsync from the OAuth.registerService('oidc')
// callback above). It adds the OIDC user to the default board (optionally
// as board admin) with no per-user authorization, so reject direct
// client/DDP calls (this.connection non-null).
if (this.connection !== null) {
throw new Meteor.Error('not-authorized');
}
check(info, Object);
check(oidcUserId, String);
+20
View File
@@ -43,6 +43,18 @@ Meteor.methods({
orgWebsite,
orgIsActive,
) {
// SECURITY (GHSA-cv95-8h7c-2ffq): This *FromOidc method is an internal
// helper invoked only server-side during the OIDC login flow (via
// Meteor.callAsync from packages/wekan-oidc/loginHandler.js). It performs
// a privileged org write with no per-user authorization, so it must never
// be callable directly by a client over DDP — otherwise any authenticated
// user could create/modify organizations, bypassing the admin-only check
// on setCreateOrg/setOrgAllFields. A server-side method call has
// this.connection === null; a direct client call has a non-null
// connection. Reject the latter.
if (this.connection !== null) {
throw new Meteor.Error('not-authorized');
}
check(orgDisplayName, String);
check(orgDesc, String);
check(orgShortName, String);
@@ -125,6 +137,14 @@ Meteor.methods({
orgWebsite,
orgIsActive,
) {
// SECURITY (GHSA-cv95-8h7c-2ffq): Internal OIDC-login-only helper (called
// server-side from packages/wekan-oidc/loginHandler.js). Performs a
// privileged org update of an arbitrary _id with no per-user
// authorization, so reject direct client/DDP calls (this.connection
// non-null) to keep the admin-only restriction of setOrgAllFields intact.
if (this.connection !== null) {
throw new Meteor.Error('not-authorized');
}
check(org, Object);
check(orgDisplayName, String);
check(orgDesc, String);
+20
View File
@@ -39,6 +39,18 @@ Meteor.methods({
teamWebsite,
teamIsActive,
) {
// SECURITY (GHSA-cv95-8h7c-2ffq): This *FromOidc method is an internal
// helper invoked only server-side during the OIDC login flow (via
// Meteor.callAsync from packages/wekan-oidc/loginHandler.js). It performs
// a privileged team write with no per-user authorization, so it must never
// be callable directly by a client over DDP — otherwise any authenticated
// user could create teams, bypassing the admin-only check on
// setCreateTeam/setTeamAllFields. A server-side method call has
// this.connection === null; a direct client call has a non-null
// connection. Reject the latter.
if (this.connection !== null) {
throw new Meteor.Error('not-authorized');
}
check(teamDisplayName, String);
check(teamDesc, String);
check(teamShortName, String);
@@ -108,6 +120,14 @@ Meteor.methods({
teamWebsite,
teamIsActive,
) {
// SECURITY (GHSA-cv95-8h7c-2ffq): Internal OIDC-login-only helper (called
// server-side from packages/wekan-oidc/loginHandler.js). Performs a
// privileged team update of an arbitrary _id with no per-user
// authorization, so reject direct client/DDP calls (this.connection
// non-null) to keep the admin-only restriction of setTeamAllFields intact.
if (this.connection !== null) {
throw new Meteor.Error('not-authorized');
}
check(team, Object);
check(teamDisplayName, String);
check(teamDesc, String);