Skip to content

BUG: Security Issue in AdminJS Fastify Integration: Privilege Escalation in Asset Handling #25

Open
@Elyasnz

Description

@Elyasnz

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:

  1. Function Ignored: If admin.options?.assets is a function, its returned values are not processed.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions