Skip to content

Brute force attack prevention#26026

Merged
arjantijms merged 5 commits into
eclipse-ee4j:mainfrom
OndroMih:ondromih-2026-05-cve-brute-force-attack
May 19, 2026
Merged

Brute force attack prevention#26026
arjantijms merged 5 commits into
eclipse-ee4j:mainfrom
OndroMih:ondromih-2026-05-cve-brute-force-attack

Conversation

@OndroMih
Copy link
Copy Markdown
Contributor

@OndroMih OndroMih commented May 9, 2026

protection against brute-force authentication attacks on the administration interface (both the Administration Console and REST API). This protection works by:

  • Tracking failed authentication attempts per username and remote host combination
  • Applying an exponential delay after each failed attempt (1 second, 2 seconds, 4 seconds, 8 seconds, etc.)
  • Capping the maximum delay at 60 seconds
  • Rejecting additional concurrent authentication attempts when too many requests are being delayed for the same user/host (HTTP 429 Too Many Requests)

This mechanism makes it impractical for attackers to try many passwords in rapid succession while allowing legitimate users to retry after a short wait.

This mechanism applies only for remote connections. Local connections are never delayed.

When the server is deployed behind a reverse proxy, the configuration option behind-proxy on the admin listener enables getting remote client's address from proxy headers.

- exponential delay for repeated failures per hostname and user, max delay 1 minute
- limit the number of concurrent requests that delay to 3 per the same hostname and user
- if requests are coming from localhost (Admin Console), take the hostname from a special header
- ignore requests with empty password - a lot of tools (e.g. IDEs) issue regular pings with an empty password
@OndroMih OndroMih requested a review from a team May 9, 2026 10:11
@dmatej
Copy link
Copy Markdown
Contributor

dmatej commented May 11, 2026

Error:  Failures: 
Error:    AsadminVerifyDomainXmlITest.verifyDomainXml:37 
Expected: asadmin succeeded
     but: was <Exception in thread "main" java.lang.NoClassDefFoundError: org/glassfish/grizzly/config/dom/NetworkListener
	at java.base/java.lang.Class.getDeclaredMethods0(Native Method)
	at java.base/java.lang.Class.privateGetDeclaredMethods(Class.java:3580)
	at java.base/java.lang.Class.privateGetPublicMethods(Class.java:3605)
	at java.base/java.lang.Class.getMethods(Class.java:2187)
	at java.base/java.lang.reflect.Proxy$ProxyBuilder.referencedTypes(Proxy.java:730)
	at java.base/java.lang.reflect.Proxy$ProxyBuilder.<init>(Proxy.java:632)
	at java.base/java.lang.reflect.Proxy$ProxyBuilder.<init>(Proxy.java:643)
	at java.base/java.lang.reflect.Proxy.lambda$getProxyConstructor$0(Proxy.java:429)
	at java.base/jdk.internal.loader.AbstractClassLoaderValue$Memoizer.get(AbstractClassLoaderValue.java:329)
	at java.base/jdk.internal.loader.AbstractClassLoaderValue.computeIfAbsent(AbstractClassLoaderValue.java:205)
	at java.base/java.lang.reflect.Proxy.getProxyConstructor(Proxy.java:427)
	at java.base/java.lang.reflect.Proxy.newProxyInstance(Proxy.java:1034)
	at org.jvnet.hk2.config.Dom$DomProxyComputable.compute(Dom.java:1575)
	at org.jvnet.hk2.config.Dom$DomProxyComputable.compute(Dom.java:1564)
	at org.glassfish.hk2.utilities.cache.internal.WeakCARCacheImpl.compute(WeakCARCacheImpl.java:108)
	at org.jvnet.hk2.config.Dom.createProxy(Dom.java:1084)
	at com.sun.enterprise.admin.servermgmt.cli.VerifyDomainXmlCommand.executeCommand(VerifyDomainXmlCommand.java:95)
	at com.sun.enterprise.admin.cli.CLICommand.execute(CLICommand.java:296)
	at com.sun.enterprise.admin.cli.AdminMain.executeCommand(AdminMain.java:304)
	at com.sun.enterprise.admin.cli.AdminMain.doMain(AdminMain.java:231)
	at org.glassfish.admin.cli.AsadminMain.main(AsadminMain.java:35)
Caused by: java.lang.ClassNotFoundException: org.glassfish.grizzly.config.dom.NetworkListener
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
	... 21 more>

@OndroMih OndroMih force-pushed the ondromih-2026-05-cve-brute-force-attack branch from 0b2a8a3 to cd56c9c Compare May 11, 2026 13:46
@OndroMih OndroMih requested a review from dmatej May 11, 2026 21:26
@OndroMih OndroMih force-pushed the ondromih-2026-05-cve-brute-force-attack branch 2 times, most recently from 8529bfb to cc74b67 Compare May 12, 2026 14:22
@OndroMih OndroMih force-pushed the ondromih-2026-05-cve-brute-force-attack branch from cc74b67 to 98db0ce Compare May 12, 2026 16:50
@OndroMih OndroMih requested a review from hs536 May 13, 2026 09:28
@dmatej dmatej added this to the 8.0.3 milestone May 13, 2026
Copy link
Copy Markdown
Contributor

@avpinchuk avpinchuk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How much memory will be consumed to store login attempts when there are a large number of attempts?

@OndroMih
Copy link
Copy Markdown
Contributor Author

How much memory will be consumed to store login attempts when there are a large number of attempts?

The memory will keep teh records only for 1 minute, so it will hold as many records as can be generated in 1 minute, a record per each different username.

it could be improved to either keep a record only per remote IP address but I wanted to avoid that to avoid complete lock out in case an attacker and and regular users would come from the same host (e.g. if proxy is used without the behind-proxy setting).

Another option to improve this is to somehow find out whether the username is valid and store attempts for invalid usernames under the same key. I'll think about this and see if there's a simple way to find out whether the username exists, not not just whether the username/password combination is valid.

data.lastFailureTime.set(System.currentTimeMillis());

// Calculate exponential delay: 2^(failures-1) seconds, capped at MAX_DELAY_SECONDS
int delaySeconds = (int) Math.min(Math.pow(2, failureCount - 1), MAX_DELAY_SECONDS);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the delay reaches MAX_DELAY_SECONDS after 6 failures, aren’t we repeatedly doing redundant Math.pow calculations and incrementing failureCount without any practical effect?
It might be better to cap the failure count at a realistic number (e.g. 10–20 attempts), and after that simply keep using a fixed delay of MAX_DELAY_SECONDS.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. I adjusted the algorithm to skip computing Math.pow if failureCount is greater than square root of the wait limit.

@OndroMih
Copy link
Copy Markdown
Contributor Author

@avpinchuk , I adjusted the tracking algorithm to track unknown usernames under the same key to prevent the hashmap from unlimited growth.

@OndroMih OndroMih requested review from avpinchuk, dmatej and hs536 May 14, 2026 19:41
@OndroMih OndroMih force-pushed the ondromih-2026-05-cve-brute-force-attack branch 3 times, most recently from d85d266 to ffdc3c6 Compare May 18, 2026 00:10
int delaySeconds = (int) Math.min(Math.pow(2, failureCount - 1), MAX_DELAY_SECONDS);
// Calculate exponential delay
int delaySeconds = failureCount - 1 < FAILURE_COUNT_REACHING_MAX_DELAY
? (int) Math.pow(2, failureCount - 1)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When failureCount=7, delaySeconds becomes 64 seconds, which exceeds MAX_DELAY_SECONDS. It's safer to keep Math.min().

- ? (int) Math.pow(2, failureCount - 1)
+ ? (int) Math.min(Math.pow(2, failureCount - 1), MAX_DELAY_SECONDS)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, @hs536 . Done, I amended my last commit.

- avoid computing square root when max attempts reached
- store non-existent usernames under the same key to prevent username enumeration attacks and unbounded tracker map growth
@OndroMih OndroMih force-pushed the ondromih-2026-05-cve-brute-force-attack branch from ffdc3c6 to c016af8 Compare May 18, 2026 10:59
@hs536
Copy link
Copy Markdown
Contributor

hs536 commented May 19, 2026

@OndroMih, let me confirm a few points. Is my understanding of the user impact from introducing this security feature correct as follows?

  • If login attempts continue to fail, the waiting time will progressively increase
  • When a reverse proxy is used, configuration changes are required on the proxy side to add the X-Real-IP and X-Forwarded-For headers
  • This feature is enabled by default, and there is currently no way to disable it

@OndroMih
Copy link
Copy Markdown
Contributor Author

Hi, @hs536 ,

yes, your understanding is correct, plus:

  • this feature is only enabled for remote connections. When accessing Admin console or REST interface from localhost, there’s no delay
  • no configuration changes are required when using proxy, the additional configuration is only optional. It allows delaying calls only from the attacker. With default configuration, calls would be delayed for everybody because they would come from the same proxy server. We need to allow getting the remote IP from headers only when behind a proxy, otherwise an atracker could specify any IP address in the headers and bypass the protection if sending random IPs each time.

This feature is enabled by default because regulations require that GlassFish is secure in the default configuration. Idesigned it carefully so that an attacker cannot lock out valid users:

  • delay is per remote IP, so only the atracker is delayed, even if they try with a valid username
  • in case the attacker shares the same IP as valid users, valid users are still able to log in without delay via a local connection. This would, for example, allow tunneled connections via SSH
  • In case of having proxy, the behind-proxy option should be optionally enabled to block only attackers’ per their IP

If this is not enough, I can also add an option to disable this feature with an additional toggle on the HTTP config, which would be enabled by default.

@arjantijms
Copy link
Copy Markdown
Contributor

If this is not enough, I can also add an option to disable this feature with an additional toggle on the HTTP config, which would be enabled by default.

If needed, let's do that in a followup PR. Additionally we could factory out some of the locking code to the authentication lib.

@arjantijms arjantijms merged commit 6cab4ce into eclipse-ee4j:main May 19, 2026
6 checks passed
@hs536 hs536 added security fix The change (component upgrade or gf code) concerns a CVE breaking change Changes something users / app devs labels May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Changes something users / app devs security fix The change (component upgrade or gf code) concerns a CVE

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants