Affected Version
This vulnerability is specific to AdminJS Fastify v4.1.3, identified in this commit. For relevant documentation, see adminjs-options.interface.ts and core-scripts.interface.ts.
Problem Overview
The AdminJSOptions assets configuration allows customization of styles, scripts, and core scripts for AdminJS. However, the withProtectedRoutesHandler function in the affected version fails to handle assets correctly. The issues are twofold:
- Function Ignored: If
admin.options?.assets is a function, its returned values are not processed.
- Improper Flattening: Objects such as
coreScripts in admin.options?.assets are incorrectly flattened using Array.prototype.flat(), causing privilege escalation by unintentionally granting unauthenticated API access.
Exploitation Scenario
Consider the following AdminJSOptions configuration:
{
assetsCDN: 'https://example.com/',
assets: {
styles: ['/path/to/style.css'],
coreScripts: {
'app.bundle.js': 'app.bundle.123456.js',
'components.bundle.js': 'components.bundle.123456.js',
'design-system.bundle.js': 'design-system.bundle.123456.js',
'global.bundle.js': 'global.bundle.123456.js',
},
},
}
and the affected code with some logs
const assets = [
...AdminRouter.assets.map((a) => a.path),
...Object.values(admin.options?.assets ?? {}).flat(),
];
console.log('AdminRouter.assets.map((a) => a.path) ->', AdminRouter.assets.map((a) => a.path))
console.log('Object.values(admin.options?.assets ?? {}).flat() ->', Object.values(admin.options?.assets ?? {}).flat())
console.log('assets ->', assets)
assets.find((a) => {
console.log(' asset ->', a)
console.log(' result ->', request.url.match(a))
return request.url.match(a);
})
console.log('assets.find((a) => request.url.match(a)) -> ', assets.find((a) => request.url.match(a)))
if (assets.find((a) => request.url.match(a))) {
console.log(1)
return;
}
When making an API request, such as:
curl -X POST 'http://localhost:8000/admin/api/resources/users/records/23/show'
The logs show that the coreScripts object is incorrectly treated as valid:
AdminRouter.assets.map((a) => a.path) -> [
'/frontend/assets/icomoon.css',
'/frontend/assets/icomoon.eot',
'/frontend/assets/icomoon.svg',
'/frontend/assets/icomoon.ttf',
'/frontend/assets/icomoon.woff',
'/frontend/assets/app.bundle.js',
'/frontend/assets/global.bundle.js',
'/frontend/assets/design-system.bundle.js',
'/frontend/assets/logo.svg',
'/frontend/assets/logo-mini.svg',
'/frontend/assets/themes/dark/theme.bundle.js',
'/frontend/assets/themes/dark/style.css',
'/frontend/assets/themes/light/theme.bundle.js',
'/frontend/assets/themes/light/style.css',
'/frontend/assets/themes/no-sidebar/theme.bundle.js',
'/frontend/assets/themes/no-sidebar/style.css'
]
Object.values(admin.options?.assets ?? {}).flat() -> [
'/path/to/style.css',
{
'app.bundle.js': 'app.bundle.123456.js',
'components.bundle.js': 'components.bundle.123456.js',
'design-system.bundle.js': 'design-system.bundle.123456.js',
'global.bundle.js': 'global.bundle.123456.js'
}
]
assets -> [
'/frontend/assets/icomoon.css',
'/frontend/assets/icomoon.eot',
'/frontend/assets/icomoon.svg',
'/frontend/assets/icomoon.ttf',
'/frontend/assets/icomoon.woff',
'/frontend/assets/app.bundle.js',
'/frontend/assets/global.bundle.js',
'/frontend/assets/design-system.bundle.js',
'/frontend/assets/logo.svg',
'/frontend/assets/logo-mini.svg',
'/frontend/assets/themes/dark/theme.bundle.js',
'/frontend/assets/themes/dark/style.css',
'/frontend/assets/themes/light/theme.bundle.js',
'/frontend/assets/themes/light/style.css',
'/frontend/assets/themes/no-sidebar/theme.bundle.js',
'/frontend/assets/themes/no-sidebar/style.css',
'/path/to/style.css',
{
'app.bundle.js': 'app.bundle.123456.js',
'components.bundle.js': 'components.bundle.123456.js',
'design-system.bundle.js': 'design-system.bundle.123456.js',
'global.bundle.js': 'global.bundle.123456.js'
}
]
asset -> /frontend/assets/icomoon.css
result -> null
asset -> /frontend/assets/icomoon.eot
result -> null
asset -> /frontend/assets/icomoon.svg
result -> null
asset -> /frontend/assets/icomoon.ttf
result -> null
asset -> /frontend/assets/icomoon.woff
result -> null
asset -> /frontend/assets/app.bundle.js
result -> null
asset -> /frontend/assets/global.bundle.js
result -> null
asset -> /frontend/assets/design-system.bundle.js
result -> null
asset -> /frontend/assets/logo.svg
result -> null
asset -> /frontend/assets/logo-mini.svg
result -> null
asset -> /frontend/assets/themes/dark/theme.bundle.js
result -> null
asset -> /frontend/assets/themes/dark/style.css
result -> null
asset -> /frontend/assets/themes/light/theme.bundle.js
result -> null
asset -> /frontend/assets/themes/light/style.css
result -> null
asset -> /frontend/assets/themes/no-sidebar/theme.bundle.js
result -> null
asset -> /frontend/assets/themes/no-sidebar/style.css
result -> null
asset -> /path/to/style.css
result -> null
asset -> {
'app.bundle.js': 'app.bundle.123456.js',
'components.bundle.js': 'components.bundle.123456.js',
'design-system.bundle.js': 'design-system.bundle.123456.js',
'global.bundle.js': 'global.bundle.123456.js'
}
result -> [
'e',
index: 12,
input: '/admin/api/resources/users/records/23/show',
groups: undefined
]
assets.find((a) => request.url.match(a)) -> {
'app.bundle.js': 'app.bundle.123456.js',
'components.bundle.js': 'components.bundle.123456.js',
'design-system.bundle.js': 'design-system.bundle.123456.js',
'global.bundle.js': 'global.bundle.123456.js'
}
1
Here, 1 indicates the request was validated, granting access without proper authentication.
Root Cause
In the preHandler hook of withProtectedRoutesHandler, the assets array includes improperly flattened values:
const assets = [
...AdminRouter.assets.map((a) => a.path),
...Object.values(admin.options?.assets ?? {}).flat(),
];
This causes objects like coreScripts to bypass route protection.
Proposed Fix
The updated code ensures proper handling of admin.options?.assets, respects functions, and validates only string-based assets:
import AdminJS, { CurrentAdmin, Router as AdminRouter } from 'adminjs';
import { FastifyInstance } from 'fastify';
export const withProtectedRoutesHandler = (
fastifyApp: FastifyInstance,
admin: AdminJS,
): void => {
const { rootPath } = admin.options;
fastifyApp.addHook('preHandler', async (request, reply) => {
const buildComponentRoute = AdminRouter.routes.find(
(r) => r.action === 'bundleComponents',
)?.path;
let AdminOptionsAssets = admin.options?.assets ?? {};
if (typeof AdminOptionsAssets === 'function')
AdminOptionsAssets = await AdminOptionsAssets(request.session.get('adminUser') as CurrentAdmin);
const assets = [
...AdminRouter.assets.map((a) => a.path),
...Object.values(AdminOptionsAssets).flat(),
];
if (assets.find((a) => typeof a === 'string' && request.url.match(a))) {
return;
} else if (buildComponentRoute && request.url.match(buildComponentRoute)) {
return;
} else if (
!request.url.startsWith(rootPath) ||
request.session.get('adminUser') ||
// these routes don't need authentication
request.url.startsWith(admin.options.loginPath) ||
request.url.startsWith(admin.options.logoutPath)
) {
return;
} else {
// If the redirection is caused by API call to some action just redirect to resource
const [redirectTo] = request.url.split('/actions');
request.session.redirectTo = redirectTo.includes(`${rootPath}/api`)
? rootPath
: redirectTo;
return reply.redirect(admin.options.loginPath);
}
});
};
Fix Highlights
- Function Respect: Functions in assets are executed and their return values are processed.
- Validation: Only string-based paths are included in assets, preventing objects like coreScripts from being validated.
- Secure Access: Unauthenticated API calls are blocked, ensuring route protection.
Impact
The fix eliminates privilege escalation risks by:
- Validating assets paths correctly.
- Blocking unauthenticated access to protected API routes.
Affected Version
This vulnerability is specific to AdminJS Fastify v4.1.3, identified in this commit. For relevant documentation, see adminjs-options.interface.ts and core-scripts.interface.ts.
Problem Overview
The
AdminJSOptionsassetsconfiguration allows customization of styles, scripts, and core scripts for AdminJS. However, thewithProtectedRoutesHandlerfunction in the affected version fails to handleassetscorrectly. The issues are twofold:admin.options?.assetsis a function, its returned values are not processed.coreScriptsinadmin.options?.assetsare incorrectly flattened usingArray.prototype.flat(), causing privilege escalation by unintentionally granting unauthenticated API access.Exploitation Scenario
Consider the following
AdminJSOptionsconfiguration:and the affected code with some logs
When making an API request, such as:
curl -X POST 'http://localhost:8000/admin/api/resources/users/records/23/show'The logs show that the
coreScriptsobject is incorrectly treated as valid:Here,
1indicates the request was validated, granting access without proper authentication.Root Cause
In the
preHandlerhook ofwithProtectedRoutesHandler, theassetsarray includes improperly flattened values:This causes objects like
coreScriptsto bypass route protection.Proposed Fix
The updated code ensures proper handling of
admin.options?.assets, respects functions, and validates only string-based assets:Fix Highlights
Impact
The fix eliminates privilege escalation risks by: