VYPR
Moderate severityOSV Advisory· Published Jan 27, 2026· Updated Jan 28, 2026

StudioCMS has an Authorization Bypass Through User-Controlled Key

CVE-2026-24134

Description

StudioCMS is a server-side-rendered, Astro native, headless content management system. Versions prior to 0.2.0 contain a Broken Object Level Authorization (BOLA) vulnerability in the Content Management feature that allows users with the "Visitor" role to access draft content created by Editor/Admin/Owner users. Version 0.2.0 patches the issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
studiocmsnpm
< 0.2.00.2.0

Affected products

1
  • Range: 0.1.0-beta.1, @astrolicious/studiocms-blog@0.1.0-beta.2, @astrolicious/studiocms-blog@0.1.0-beta.3, …

Patches

1
efc10bee20db

feat(auth): implement middleware-level permission checks for dashboard routes (#1214)

https://github.com/withstudiocms/studiocmsAdam MatthiesenJan 23, 2026via ghsa
5 files changed · +122 0
  • .changeset/metal-trains-lead.md+5 0 added
    @@ -0,0 +1,5 @@
    +---
    +"studiocms": patch
    +---
    +
    +Fix: Reworks permission checks for dashboard routes to be at the middleware level to prevent unauthorized access
    
  • packages/studiocms/frontend/middleware/_authmap.ts+63 0 added
    @@ -0,0 +1,63 @@
    +import { dashboardConfig } from 'studiocms:config';
    +
    +type AuthenticatedRoute = {
    +	pathname: string;
    +	requiredPermissionLevel: 'owner' | 'admin' | 'editor' | 'visitor';
    +};
    +
    +// Import the dashboard route override from the configuration
    +// If no override is set, it defaults to 'dashboard'
    +// This allows for flexibility in the dashboard route without hardcoding it
    +const dashboardRoute = dashboardConfig.dashboardRouteOverride || 'dashboard';
    +
    +/**
    + * List of authenticated routes with their required permission levels.
    + * This list is used to determine if a user has the necessary permissions
    + * to access specific dashboard routes.
    + */
    +export const authenticatedRoutes: AuthenticatedRoute[] = [
    +	{
    +		pathname: `/${dashboardRoute}/system-management`,
    +		requiredPermissionLevel: 'owner',
    +	},
    +	{
    +		pathname: `/${dashboardRoute}/smtp-configuration`,
    +		requiredPermissionLevel: 'owner',
    +	},
    +	{
    +		pathname: `/${dashboardRoute}`,
    +		requiredPermissionLevel: 'editor',
    +	},
    +	{
    +		pathname: `/${dashboardRoute}/user-management`,
    +		requiredPermissionLevel: 'admin',
    +	},
    +	{
    +		pathname: `/${dashboardRoute}/user-management/**`,
    +		requiredPermissionLevel: 'admin',
    +	},
    +	{
    +		pathname: `/${dashboardRoute}/taxonomy`,
    +		requiredPermissionLevel: 'editor',
    +	},
    +	{
    +		pathname: `/${dashboardRoute}/taxonomy/categories`,
    +		requiredPermissionLevel: 'editor',
    +	},
    +	{
    +		pathname: `/${dashboardRoute}/taxonomy/tags`,
    +		requiredPermissionLevel: 'editor',
    +	},
    +	{
    +		pathname: `/${dashboardRoute}/plugins/**`,
    +		requiredPermissionLevel: 'editor',
    +	},
    +	{
    +		pathname: `/${dashboardRoute}/content-management`,
    +		requiredPermissionLevel: 'editor',
    +	},
    +	{
    +		pathname: `/${dashboardRoute}/content-management/**`,
    +		requiredPermissionLevel: 'editor',
    +	},
    +];
    
  • packages/studiocms/frontend/middleware/index.ts+46 0 modified
    @@ -19,7 +19,9 @@ import { SDKCore } from 'studiocms:sdk';
     import SCMSUiVersion from 'studiocms:ui/version';
     import SCMSVersion from 'studiocms:version';
     import { defineMiddlewareRouter, Effect } from '@withstudiocms/effect';
    +import micromatch from 'micromatch';
     import { STUDIOCMS_EDITOR_CSRF_COOKIE_NAME } from '#consts';
    +import { authenticatedRoutes } from './_authmap.js';
     import { getUserPermissions, makeFallbackSiteConfig, SetLocal, setLocals } from './utils.js';
     
     // Import the dashboard route override from the configuration
    @@ -145,6 +147,50 @@ export const onRequest = defineMiddlewareRouter([
     			// Check if the user is logged in and redirect to the login page if not
     			if (!userSessionData.isLoggedIn) return context.redirect(StudioCMSRoutes.authLinks.loginURL);
     
    +			// Get the current path
    +			const currentPath = context.url.pathname;
    +
    +			// Check if the user has permission to access the current route
    +			// If not, redirect to the dashboard home page
    +			const userPermissionLevel =
    +				context.locals.StudioCMS.security?.userSessionData.permissionLevel;
    +
    +			if (!userPermissionLevel) {
    +				// How did the user get here? Log them out to reset session
    +				return context.redirect(`/${dashboardRoute}/logout`);
    +			}
    +
    +			// Using micromatch to handle wildcard route matching
    +			const matchChance1 = authenticatedRoutes.find((route) =>
    +				micromatch.isMatch(currentPath, route.pathname)
    +			);
    +
    +			// if trailing `/` exists, try matching without it
    +			const matchChance2 =
    +				matchChance1 ||
    +				(() => {
    +					if (currentPath.endsWith('/')) {
    +						const trimmedPath = currentPath.slice(0, -1);
    +						return authenticatedRoutes.find((route) =>
    +							micromatch.isMatch(trimmedPath, route.pathname)
    +						);
    +					}
    +					return null;
    +				})();
    +
    +			if (matchChance1 || matchChance2) {
    +				// biome-ignore lint/style/noNonNullAssertion: only used after checking for existence
    +				const matchingRoute = matchChance1 || matchChance2!;
    +				const requiredLevel = matchingRoute.requiredPermissionLevel;
    +				const levels = ['visitor', 'editor', 'admin', 'owner'];
    +				const userLevelIndex = levels.indexOf(userPermissionLevel);
    +				const requiredLevelIndex = levels.indexOf(requiredLevel);
    +
    +				if (userLevelIndex < requiredLevelIndex) {
    +					return context.redirect(`/${dashboardRoute}`);
    +				}
    +			}
    +
     			// Else, Continue to the next middleware
     			return next();
     		}),
    
  • packages/studiocms/package.json+2 0 modified
    @@ -257,6 +257,7 @@
     		"jose": "^6.1.3",
     		"micromark": "^4.0.2",
     		"micromark-extension-gfm": "^3.0.0",
    +		"micromatch": "^4.0.8",
     		"magicast": "^0.5.1",
     		"mdast-util-to-markdown": "^2.1.2",
     		"mrmime": "^2.0.1",
    @@ -270,6 +271,7 @@
     	},
     	"devDependencies": {
     		"@types/mdast": "^4.0.4",
    +		"@types/micromatch": "^4.0.10",
     		"@types/node": "catalog:",
     		"@types/semver": "^7.7.1",
     		"@types/three": "0.169.0",
    
  • pnpm-lock.yaml+6 0 modified
    @@ -1113,6 +1113,9 @@ importers:
           micromark-extension-gfm:
             specifier: ^3.0.0
             version: 3.0.0
    +      micromatch:
    +        specifier: ^4.0.8
    +        version: 4.0.8
           mrmime:
             specifier: ^2.0.1
             version: 2.0.1
    @@ -1147,6 +1150,9 @@ importers:
           '@types/mdast':
             specifier: ^4.0.4
             version: 4.0.4
    +      '@types/micromatch':
    +        specifier: ^4.0.10
    +        version: 4.0.10
           '@types/node':
             specifier: 'catalog:'
             version: 22.19.6
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.