VYPR
High severity8.5NVD Advisory· Published Jun 2, 2026

CVE-2026-49120

CVE-2026-49120

Description

Medplum before 5.1.14 contains a server-side request forgery vulnerability in the subscription worker that allows authenticated users to perform unauthorized internal network requests by creating FHIR Subscription resources with arbitrary endpoint URLs. Attackers can point subscription endpoints at internal addresses such as cloud instance metadata services, internal databases, or container orchestration endpoints to exfiltrate IAM credentials and patient health records via the POST body containing full FHIR resource payloads.

Affected products

1

Patches

1
87595e98d756

Require HTTPS for rest-hook Subscription URLs by default (#9334)

https://github.com/medplum/medplumCody EbbersonMay 29, 2026via nvd-ref
4 files changed · +90 0
  • packages/server/src/config/types.ts+3 0 modified
    @@ -201,6 +201,9 @@ export interface MedplumServerConfig {
        * Optional flag to require email verification before allowing users to create projects.
        */
       requireVerifiedEmailForProjectCreation?: boolean;
    +
    +  /** Optional flag to allow rest-hook Subscriptions to send requests to insecure HTTP URLs. */
    +  allowInsecureRestHookUrl?: boolean;
     }
     
     export interface SubscriptionAutoDisableTrigger {
    
  • packages/server/src/config/utils.ts+1 0 modified
    @@ -160,6 +160,7 @@ export function isFloatConfig(_key: string): boolean {
     }
     
     const booleanKeys = new Set([
    +  'allowInsecureRestHookUrl',
       'botCustomFunctionsEnabled',
       'database.ssl.rejectUnauthorized',
       'database.ssl.require',
    
  • packages/server/src/workers/subscription.test.ts+65 0 modified
    @@ -537,6 +537,71 @@ describe('Subscription Worker', () => {
           await expect(findAndExecSubscriptionJob(patient, 'create')).rejects.toThrow('Job not found');
         }));
     
    +  test('Reject insecure rest-hook URLs by default', () =>
    +    withTestContext(async () => {
    +      const subscription = await repo.createResource<Subscription>({
    +        resourceType: 'Subscription',
    +        reason: 'test',
    +        status: 'active',
    +        criteria: 'Patient',
    +        channel: {
    +          type: 'rest-hook',
    +          endpoint: 'http://example.com/subscription',
    +        },
    +      });
    +      expect(subscription).toBeDefined();
    +
    +      const patient = await repo.createResource<Patient>({
    +        resourceType: 'Patient',
    +        name: [{ given: ['Alice'], family: 'Smith' }],
    +      });
    +      expect(patient).toBeDefined();
    +
    +      await expect(findAndExecSubscriptionJob(patient, 'create')).rejects.toThrow('HTTPS is required');
    +      expect(fetch).not.toHaveBeenCalled();
    +    }));
    +
    +  test('Allow insecure rest-hook URLs when configured', () =>
    +    withTestContext(async () => {
    +      const url = 'http://example.com/subscription';
    +      const savedConfig = getConfig().allowInsecureRestHookUrl;
    +      getConfig().allowInsecureRestHookUrl = true;
    +
    +      try {
    +        const subscription = await repo.createResource<Subscription>({
    +          resourceType: 'Subscription',
    +          reason: 'test',
    +          status: 'active',
    +          criteria: 'Patient',
    +          channel: {
    +            type: 'rest-hook',
    +            endpoint: url,
    +          },
    +        });
    +        expect(subscription).toBeDefined();
    +
    +        const patient = await repo.createResource<Patient>({
    +          resourceType: 'Patient',
    +          name: [{ given: ['Alice'], family: 'Smith' }],
    +        });
    +        expect(patient).toBeDefined();
    +
    +        (fetch as unknown as jest.Mock).mockImplementation(() => ({ status: 200 }));
    +
    +        await findAndExecSubscriptionJob(patient, 'create');
    +
    +        expect(fetch).toHaveBeenCalledWith(
    +          url,
    +          expect.objectContaining({
    +            method: 'POST',
    +            body: stringify(patient),
    +          })
    +        );
    +      } finally {
    +        getConfig().allowInsecureRestHookUrl = savedConfig;
    +      }
    +    }));
    +
       // Skip test
       test.skip('Ignore subscriptions with missing criteria', () =>
         withTestContext(async () => {
    
  • packages/server/src/workers/subscription.ts+21 0 modified
    @@ -40,6 +40,7 @@ import fetch from 'node-fetch';
     import { createHmac } from 'node:crypto';
     import type { Operation } from 'rfc6902';
     import { executeBot } from '../bots/execute';
    +import { getConfig } from '../config/loader';
     import type { SubscriptionAutoDisableTrigger } from '../config/types';
     import { WEBSOCKET_SUB_PUBLISH_CHANNEL } from '../constants';
     import { getRequestContext, runInAuthenticatedContext, tryGetRequestContext, tryRunInRequestContext } from '../context';
    @@ -730,6 +731,7 @@ async function sendRestHook(
         systemRepo = getGlobalSystemRepo(); // SHARDING is global correct if no project?
       }
       try {
    +    validateRestHookUrl(url);
         log.info('Sending rest hook', {
           url,
           subscriptionId: subscription.id,
    @@ -787,6 +789,25 @@ async function sendRestHook(
       }
     }
     
    +function validateRestHookUrl(url: string): void {
    +  let parsedUrl: URL;
    +  try {
    +    parsedUrl = new URL(url);
    +  } catch {
    +    throw new Error('Invalid rest-hook URL: must be an absolute HTTPS URL');
    +  }
    +
    +  if (parsedUrl.protocol === 'https:') {
    +    return;
    +  }
    +
    +  if (parsedUrl.protocol === 'http:' && getConfig().allowInsecureRestHookUrl) {
    +    return;
    +  }
    +
    +  throw new Error('Invalid rest-hook URL: HTTPS is required unless allowInsecureRestHookUrl is enabled');
    +}
    +
     /**
      * Builds a collection of HTTP request headers for the rest-hook subscription.
      * @param job - The subscription job.
    

Vulnerability mechanics

Root cause

"The subscription worker did not validate the protocol of the endpoint URL for rest-hook subscriptions, allowing HTTP."

Attack vector

An authenticated user can create a FHIR Subscription resource with a `rest-hook` channel type. By setting the `endpoint` to an arbitrary HTTP URL, the attacker can trick the server into sending sensitive data, such as patient health records or IAM credentials, to an attacker-controlled server. This is possible because the subscription worker processes these resources and sends POST requests to the specified endpoints with the FHIR resource payload in the body [ref_id=1].

Affected code

The vulnerability lies within the `sendRestHook` function in `packages/server/src/workers/subscription.ts`, which is responsible for sending notifications to rest-hook endpoints. The `validateRestHookUrl` function was added to enforce URL validation.

What the fix does

The patch introduces a `validateRestHookUrl` function that enforces HTTPS for subscription endpoint URLs by default. It also adds a configuration option `allowInsecureRestHookUrl` which, if enabled, permits HTTP URLs. This change prevents the server from sending data to arbitrary HTTP endpoints, mitigating the risk of data exfiltration [patch_id=4524227].

Preconditions

  • authThe attacker must be authenticated.

Generated on Jun 2, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.