Skip to content

TOCTOU race in max-instances check allows unauthenticated container spawn beyond configured limits #591

Description

@Lukeesec

Summary

Concurrent requests bypass the max-instances limit and spawn containers beyond the configured cap. Any internet visitor triggers this with parallel requests.

Vulnerability Details

validateMaxInstances() at BaseController.java:262-281 reads the current proxy count, then the caller starts a proxy — two separate operations with no lock between them:

long currentAmountOfInstances = proxyService.getUserProxiesBySpecId(spec.getId()).count();
return currentAmountOfInstances < maxInstances;

POST /app_i/{specId}/{appInstanceName} at AppController.java:283 starts proxies via asyncProxyService.startProxy() — asynchronous, returns immediately. The new proxy does not
appear in the active list until async initialization completes. Every concurrent request that hits the count query before the first proxy registers sees the stale count and
passes.

Each request uses a different instance name (inst_1, inst_2, etc.), so the instance name uniqueness check at AppController.java:264-266 does not block them.

GET /app_direct_i/** at AppDirectController.java:96-106 has the same race, with the added cost that each request blocks a server thread for up to 10 minutes
(AppDirectController.java:116-132).

max-total-instances is only a config setter passed to ContainerProxy (ShinyProxySpecProvider.java:572-577). It is never enforced in ShinyProxy code.

Steps to Reproduce

  1. Configure max-instances: 2 for an app spec
  2. Send 20 concurrent POST requests with unique instance names:
    for i in $(seq 1 20); do
    curl -s -X POST "https://target/app_i/myapp/inst_$i" &
    done
  3. All 20 pass validateMaxInstances() and start containers

Impact

An attacker exhausts Docker or Kubernetes compute by spawning containers past the configured limit. On the app_direct path, the 10-minute blocking wait simultaneously
exhausts the Undertow thread pool, causing full denial of service.

Activity

  1. added theissue type on Apr 7, 2026
  2. added this to the Next milestone on Apr 7, 2026
  3. LEDfan commented on Apr 7, 2026

    @LEDfan
    Member

    Hi, first of all, thanks for opening this issue.

    When implementing this feature we were aware there could be an overshoot in the number of allowed instances. The reason is that implementing a lock also comes with some risks, e.g. around deadlocks and implementing it in a fast way when using multiple replicas.

    Nevertheless, it seems the overshoot is higher compared to what we originally expected and tested. This could be because of some refactors we did.
    We'll therefore fix this in the next release.

    On the app_direct path, the 10-minute blocking wait simultaneously exhausts the Undertow thread pool, causing full denial of service.

    This is correct and this is also the reason we no longer use the app_direct endpoint in the UI itself. But the endpoint still works, so it could be used to lock the server. For the next release I'll disable the app_direct endpoint by default, and add an option to enable it.

  4. fproske commented on Jun 23, 2026

    @fproske

    We use app_direct for showcasing apps on our website using direct links. We also noticed that the max-total-instances does not work reliably and have therefore enforced a limit on the Kubernetes side instead.

    It would be a shame if the app_direct option would slowly be phased out as embedding apps on websites is a great feature of shinyproxy!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions