Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Elyasnz opened this issue Dec 25, 2024 · 0 comments · May be fixed by #26
Open

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

Elyasnz opened this issue Dec 25, 2024 · 0 comments · May be fixed by #26

Comments

@Elyasnz
Copy link

Elyasnz commented Dec 25, 2024

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.
@Elyasnz Elyasnz changed the title Security Issue in AdminJS Fastify Integration: Privilege Escalation in Asset Handling BUG: Security Issue in AdminJS Fastify Integration: Privilege Escalation in Asset Handling Dec 25, 2024
@Elyasnz Elyasnz linked a pull request Dec 25, 2024 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant