[ 
https://issues.apache.org/jira/browse/SLING-13353?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Rishabh Daim updated SLING-13353:
---------------------------------
    Description: 
h4. Summary

{{SlingAuthenticator}} can recurse indefinitely between 
{{handleLoginFailure(...)}} and {{getAnonymousResolver(...)}} when acquiring 
the anonymous {{ResourceResolver}} keeps failing with a {{LoginException}} 
(e.g. the backing repository is temporarily unavailable). The mutual recursion 
grows the call stack until a {{StackOverflowError}} is thrown, and because each 
failed request logs the full repeating stack trace, a sustained failure 
produces a very large volume of error logging (observed in the field as an 
error log growing until the disk filled).
h4. Where

{{org.apache.sling.auth.core.impl.SlingAuthenticator}} (auth-core bundle). Two 
methods call each other with no termination guard:
 * {{{}getAnonymousResolver(...){}}}: on {{{}isAnonAllowed(request){}}}, it 
calls {{{}resourceResolverFactory.getResourceResolver(authInfo){}}}; if that 
throws {{{}LoginException{}}}, the {{catch}} block calls 
{{{}handleLoginFailure(request, response, new AuthenticationInfo(null, 
"anonymous user"), re){}}}.
 * {{{}handleLoginFailure(...){}}}: for a {{{}LoginException{}}}, when 
{{isAnonAllowed(request)}} is still true (and not an auth-handler / validate 
request), it calls {{{}getAnonymousResolver(request, response, new 
AuthenticationInfo(null)){}}}.

So {{getAnonymousResolver -> handleLoginFailure -> getAnonymousResolver -> 
...}} repeats without bound as long as the anonymous {{getResourceResolver}} 
keeps throwing {{{}LoginException{}}}.
h4. Steps to reproduce
 # Anonymous access is allowed for the requested path (default configuration).
 # Make {{ResourceResolverFactory.getResourceResolver(...)}} fail with 
{{LoginException}} for the anonymous credentials (e.g. the repository is 
unavailable).
 # Issue a plain (non-validate, non-auth-handler) request to an anonymously 
accessible path.

Expected: the anonymous login is attempted once; on failure the request is 
terminated (credentials requested / error response).
Actual: {{handleLoginFailure}} and {{getAnonymousResolver}} recurse until 
{{{}StackOverflowError{}}}; the failure (with its large stack trace) is logged 
for every affected request.
h4. Root cause

The anonymous fallback assumes that if primary authentication fails, falling 
back to anonymous will either succeed or fail terminally. It does not handle 
the case where the anonymous acquisition itself fails repeatedly: 
{{{}getAnonymousResolver{}}}'s failure path re-enters 
{{{}handleLoginFailure{}}}, which re-enters {{{}getAnonymousResolver{}}}. There 
is no "anonymous already attempted" guard and no depth bound.
h4. Proposed fix

Add a per-request re-entrancy guard: {{getAnonymousResolver}} marks (via a 
request attribute) that anonymous resolution has been attempted; 
{{handleLoginFailure}} only falls back to {{getAnonymousResolver}} when that 
mark is absent. This attempts the anonymous fallback at most once per request 
and, on repeated failure, terminates normally instead of recursing. Behaviour 
for all currently-successful paths (including a single legitimate anonymous 
fallback after a primary-auth failure) is unchanged.

A regression test drives {{getAnonymousResolver}} with a 
{{ResourceResolverFactory}} that always throws {{LoginException}} and asserts 
that (a) no {{StackOverflowError}} occurs and (b) {{getResourceResolver}} is 
invoked exactly once. Without the guard the test fails with a 
{{StackOverflowError}} (thousands of recursive invocations); with the guard it 
passes.

  was:
h4. Summary

{{SlingAuthenticator}} can recurse indefinitely between 
{{handleLoginFailure(...)}} and {{getAnonymousResolver(...)}} when acquiring 
the anonymous {{ResourceResolver}} keeps failing with a {{LoginException}} 
(e.g. the backing repository is temporarily unavailable). The mutual recursion 
grows the call stack until a {{StackOverflowError}} is thrown, and because each 
failed request logs the full repeating stack trace, a sustained failure 
produces a very large volume of error logging (observed in the field as an 
error log growing until the disk filled).

h4. Where

{{org.apache.sling.auth.core.impl.SlingAuthenticator}} (auth-core bundle). Two 
methods call each other with no termination guard:

* {{getAnonymousResolver(...)}}: on {{isAnonAllowed(request)}}, it calls 
{{resourceResolverFactory.getResourceResolver(authInfo)}}; if that throws 
{{LoginException}}, the {{catch}} block calls {{handleLoginFailure(request, 
response, new AuthenticationInfo(null, "anonymous user"), re)}}.
* {{handleLoginFailure(...)}}: for a {{LoginException}}, when 
{{isAnonAllowed(request)}} is still true (and not an auth-handler / validate 
request), it calls {{getAnonymousResolver(request, response, new 
AuthenticationInfo(null))}}.

So {{getAnonymousResolver -> handleLoginFailure -> getAnonymousResolver -> 
...}} repeats without bound as long as the anonymous {{getResourceResolver}} 
keeps throwing {{LoginException}}.

h4. Steps to reproduce

# Anonymous access is allowed for the requested path (default configuration).
# Make {{ResourceResolverFactory.getResourceResolver(...)}} fail with 
{{LoginException}} for the anonymous credentials (e.g. the repository is 
unavailable).
# Issue a plain (non-validate, non-auth-handler) request to an anonymously 
accessible path.

Expected: the anonymous login is attempted once; on failure the request is 
terminated (credentials requested / error response).
Actual: {{handleLoginFailure}} and {{getAnonymousResolver}} recurse until 
{{StackOverflowError}}; the failure (with its large stack trace) is logged for 
every affected request.

h4. Root cause

The anonymous fallback assumes that if primary authentication fails, falling 
back to anonymous will either succeed or fail terminally. It does not handle 
the case where the anonymous acquisition itself fails repeatedly: 
{{getAnonymousResolver}}'s failure path re-enters {{handleLoginFailure}}, which 
re-enters {{getAnonymousResolver}}. There is no "anonymous already attempted" 
guard and no depth bound.

h4. Proposed fix

Add a per-request re-entrancy guard: {{getAnonymousResolver}} marks (via a 
request attribute) that anonymous resolution has been attempted; 
{{handleLoginFailure}} only falls back to {{getAnonymousResolver}} when that 
mark is absent. This attempts the anonymous fallback at most once per request 
and, on repeated failure, terminates normally instead of recursing. Behaviour 
for all currently-successful paths (including a single legitimate anonymous 
fallback after a primary-auth failure) is unchanged.

A regression test drives {{getAnonymousResolver}} with a 
{{ResourceResolverFactory}} that always throws {{LoginException}} and asserts 
that (a) no {{StackOverflowError}} occurs and (b) {{getResourceResolver}} is 
invoked exactly once. Without the guard the test fails with a 
{{StackOverflowError}} (thousands of recursive invocations); with the guard it 
passes.

Affected: current trunk (auth-core bundle 66) and earlier releases carrying the 
same {{handleLoginFailure}}/{{getAnonymousResolver}} structure.



> SlingAuthenticator: unbounded handleLoginFailure <-> getAnonymousResolver 
> recursion when anonymous login repeatedly fails
> -------------------------------------------------------------------------------------------------------------------------
>
>                 Key: SLING-13353
>                 URL: https://issues.apache.org/jira/browse/SLING-13353
>             Project: Sling
>          Issue Type: Bug
>            Reporter: Rishabh Daim
>            Priority: Major
>
> h4. Summary
> {{SlingAuthenticator}} can recurse indefinitely between 
> {{handleLoginFailure(...)}} and {{getAnonymousResolver(...)}} when acquiring 
> the anonymous {{ResourceResolver}} keeps failing with a {{LoginException}} 
> (e.g. the backing repository is temporarily unavailable). The mutual 
> recursion grows the call stack until a {{StackOverflowError}} is thrown, and 
> because each failed request logs the full repeating stack trace, a sustained 
> failure produces a very large volume of error logging (observed in the field 
> as an error log growing until the disk filled).
> h4. Where
> {{org.apache.sling.auth.core.impl.SlingAuthenticator}} (auth-core bundle). 
> Two methods call each other with no termination guard:
>  * {{{}getAnonymousResolver(...){}}}: on {{{}isAnonAllowed(request){}}}, it 
> calls {{{}resourceResolverFactory.getResourceResolver(authInfo){}}}; if that 
> throws {{{}LoginException{}}}, the {{catch}} block calls 
> {{{}handleLoginFailure(request, response, new AuthenticationInfo(null, 
> "anonymous user"), re){}}}.
>  * {{{}handleLoginFailure(...){}}}: for a {{{}LoginException{}}}, when 
> {{isAnonAllowed(request)}} is still true (and not an auth-handler / validate 
> request), it calls {{{}getAnonymousResolver(request, response, new 
> AuthenticationInfo(null)){}}}.
> So {{getAnonymousResolver -> handleLoginFailure -> getAnonymousResolver -> 
> ...}} repeats without bound as long as the anonymous {{getResourceResolver}} 
> keeps throwing {{{}LoginException{}}}.
> h4. Steps to reproduce
>  # Anonymous access is allowed for the requested path (default configuration).
>  # Make {{ResourceResolverFactory.getResourceResolver(...)}} fail with 
> {{LoginException}} for the anonymous credentials (e.g. the repository is 
> unavailable).
>  # Issue a plain (non-validate, non-auth-handler) request to an anonymously 
> accessible path.
> Expected: the anonymous login is attempted once; on failure the request is 
> terminated (credentials requested / error response).
> Actual: {{handleLoginFailure}} and {{getAnonymousResolver}} recurse until 
> {{{}StackOverflowError{}}}; the failure (with its large stack trace) is 
> logged for every affected request.
> h4. Root cause
> The anonymous fallback assumes that if primary authentication fails, falling 
> back to anonymous will either succeed or fail terminally. It does not handle 
> the case where the anonymous acquisition itself fails repeatedly: 
> {{{}getAnonymousResolver{}}}'s failure path re-enters 
> {{{}handleLoginFailure{}}}, which re-enters {{{}getAnonymousResolver{}}}. 
> There is no "anonymous already attempted" guard and no depth bound.
> h4. Proposed fix
> Add a per-request re-entrancy guard: {{getAnonymousResolver}} marks (via a 
> request attribute) that anonymous resolution has been attempted; 
> {{handleLoginFailure}} only falls back to {{getAnonymousResolver}} when that 
> mark is absent. This attempts the anonymous fallback at most once per request 
> and, on repeated failure, terminates normally instead of recursing. Behaviour 
> for all currently-successful paths (including a single legitimate anonymous 
> fallback after a primary-auth failure) is unchanged.
> A regression test drives {{getAnonymousResolver}} with a 
> {{ResourceResolverFactory}} that always throws {{LoginException}} and asserts 
> that (a) no {{StackOverflowError}} occurs and (b) {{getResourceResolver}} is 
> invoked exactly once. Without the guard the test fails with a 
> {{StackOverflowError}} (thousands of recursive invocations); with the guard 
> it passes.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to