Skip to content

SystemAuthenticator.begin()/end() affect other threads of the same HTTP session #5693

Description

@glebfox

Environment

Jmix version: 2.8.5, 3.0.2

Description

SystemAuthenticator.begin()/end() (and therefore withSystem, runWithSystem, withUser, runWithUser) are documented to change the authentication of the current thread only. In fact they affect all threads of the same HTTP session: UI request threads, background tasks started from a view, and push requests.

As a result, while a withSystem block runs in a background task, UI requests of the same session run as system: full permissions, row-level policies bypassed, audit attributes recorded as system. If two begin()/end() pairs overlap on two threads of the same session, the session stays authenticated as system until logout.

Other sessions are not affected.

Reported at https://forum.jmix.io/t/systemauthenticator-begin-end-can-result-in-priviledge-escalation/7651

Steps to reproduce

In a view of a project with a logged-in admin:

@Autowired private UiAsyncTasks uiAsyncTasks;
@Autowired private SystemAuthenticator systemAuthenticator;
@Autowired private CurrentAuthentication currentAuthentication;
@Autowired private Notifications notifications;

@Subscribe(id = "longSystemTaskBtn", subject = "clickListener")
public void onLongSystemTaskBtnClick(ClickEvent<JmixButton> event) {
    uiAsyncTasks.runnableConfigurer(() ->
            systemAuthenticator.runWithSystem(() -> sleep(10_000)))
        .runAsync();
}

@Subscribe(id = "overlappingTasksBtn", subject = "clickListener")
public void onOverlappingTasksBtnClick(ClickEvent<JmixButton> event) {
    uiAsyncTasks.runnableConfigurer(() ->
            systemAuthenticator.runWithSystem(() -> sleep(3_000)))
        .runAsync();
    uiAsyncTasks.runnableConfigurer(() -> {
            sleep(1_000);
            systemAuthenticator.runWithSystem(() -> sleep(5_000));
        })
        .runAsync();
}

@Subscribe(id = "whoAmIBtn", subject = "clickListener")
public void onWhoAmIBtnClick(ClickEvent<JmixButton> event) {
    notifications.show(currentAuthentication.getAuthentication().getClass().getSimpleName()
            + " / " + currentAuthentication.getUser().getUsername());
}

private static void sleep(long millis) {
    try {
        Thread.sleep(millis);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}
  1. Log in as admin and click whoAmI.
  2. Click longSystemTask, then click whoAmI within 10 seconds.
  3. Wait 10 seconds and click whoAmI again.
  4. Click overlappingTasks, wait 10 seconds, then click whoAmI and open a view that admin is not allowed to open.

Actual behavior

  • Step 1 shows UsernamePasswordAuthenticationToken / admin.
  • Step 2 shows SystemAuthenticationToken / system — the UI request runs as system.
  • Step 3 shows admin again.
  • Step 4 still shows SystemAuthenticationToken / system after both tasks have finished, and the restricted view opens. The session stays system until logout.

Expected behavior

begin()/end() affect only the current thread. Other threads of the same session always see the user's own authentication, and overlapping begin()/end() pairs never leave the session with another authentication.

Activity

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

Metadata

Metadata

Assignees

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions