- Published on
Why Attackers Target Path-Based Servlets in AEM (And What to Do Instead)
- Authors

- Name
- Khalil
- @Im_Khalil
If you have done code reviews on an AEM project, you have almost certainly seen servlets registered like this:
@Component(
service = Servlet.class,
property = {
"sling.servlet.paths=/bin/myproject/userdata",
"sling.servlet.methods=GET"
}
)
public class UserDataServlet extends SlingSafeMethodsServlet {
// ...
}
It is easy to understand why developers write them. It feels straightforward: you need an endpoint for an AJAX call, you pick a path like /bin/myproject/userdata, and you are done in two minutes.
Adobe’s documentation and SonarQube rules explicitly warn against using sling.servlet.paths. But why is this pattern considered such a massive security risk in real-world deployments?
Let's look at what actually happens under the hood when you register a path-based servlet, how attackers target them, and the proper architectural pattern to replace them.
1. How Path-Based Servlets Break the AEM Security Model
AEM's entire security architecture is built on top of Sling Resource Resolution and JCR Access Control Lists (ACLs).
When a user requests a normal page or component:
- Sling resolves the URL to a specific JCR node in the repository (e.g.,
/content/my-site/us/en/jcr:content/root/hero). - The JCR security layer checks whether the requesting user (or
anonymous) hasjcr:readpermissions on that node. - If permissions fail, the request is rejected with a
404or403before any component script or servlet code executes.
The Problem with sling.servlet.paths:
Path-based servlets exist outside the JCR content tree. When a request hits /bin/myproject/userdata:
- There is no JCR node at that path.
- Sling resolves the request directly to the Java OSGi service in memory.
- No JCR ACL checks are evaluated.
Unless the developer manually writes explicit session checks, token verification, and permission logic inside the Java code, the endpoint is completely open to anyone who hits it.
2. The Attacker's Playbook: Automated Reconnaissance
Attackers attacking AEM instances do not guess randomly. They use automated directory fuzzers (like ffuf, gobuster, or nuclei) loaded with wordlists tailored specifically for AEM and Sling.
Common path prefixes like /bin/, /services/, /apps/, and /var/ are the first things targeted:
https://example.com/bin/query
https://example.com/bin/myproject/export
https://example.com/bin/user-info
https://example.com/bin/newsletter/subscribers
If a developer wrote a path-based servlet for an internal dashboard or quick export tool and forgot to enforce authentication, an external attacker will discover it within minutes of scanning.
Worse, if that servlet uses a ServiceResourceResolver to read data from the repository, the attacker effectively gets root-level access to private repository nodes.
3. The Dispatcher Filter Dilemma
To make a path-based servlet accessible to frontend users, developers have to open the path in the Dispatcher configuration (filters.any):
# Anti-pattern: Opening up /bin paths in Dispatcher
/0100 { /type "allow" /method "GET" /url "/bin/myproject/*" }
Using wildcards like /bin/myproject/* in Dispatcher filters is dangerous. If someone later adds a new path-based servlet under /bin/myproject/admin-tools intended only for internal testing, the Dispatcher rule inadvertently exposes it to the public internet.
4. The Clean Solution: Resource-Type Servlets
Instead of binding a servlet to an arbitrary URL string, bind it to a Resource Type, Selector, and Extension:
package com.myproject.core.servlets;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.osgi.service.component.annotations.Component;
import javax.servlet.Servlet;
import java.io.IOException;
@Component(
service = Servlet.class,
property = {
"sling.servlet.resourceTypes=myproject/components/userprofile",
"sling.servlet.selectors=data",
"sling.servlet.extensions=json",
"sling.servlet.methods=GET"
}
)
public class UserProfileDataServlet extends SlingSafeMethodsServlet {
@Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws IOException {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
// request.getResource() points to the actual JCR resource
// JCR permissions were ALREADY verified before reaching this line
response.getWriter().write("{\"status\":\"ok\"}");
}
}
Why This Is Far More Secure:
- Inherited Repository ACLs: If the user cannot access the page or resource
/content/my-site/us/en/profile/jcr:content/userprofile, Sling returns a404/403immediately. The Java servlet code is never executed. - Standard Dispatcher Rules: You don't need to open
/bin/in your Dispatcher. Standard content paths (/content/my-site/*) with.jsonextensions and.dataselectors are already handled cleanly by your standard content filters. - No URL Guessing: Attackers scanning
/bin/will find nothing.
What If You Need a Global Utility Servlet?
If you truly need a global endpoint that isn't tied to a specific page component (for example, a site-wide search or contact form submission):
Option A (Recommended): Bind the servlet to
sling/servlet/defaultwith a strict, unique selector:property = { "sling.servlet.resourceTypes=sling/servlet/default", "sling.servlet.selectors=site-search", "sling.servlet.extensions=json", "sling.servlet.methods=GET" }Users invoke it against an existing content path:
/content/my-site/us/en.site-search.json.Option B: Create a dedicated node under
/content/services/my-servicewith appropriate permissions, and bind the servlet to that resource type.
Summary Checklist
- Avoid
sling.servlet.pathsin production code. - Register servlets using
sling.servlet.resourceTypes+ selectors + extensions. - Never open broad wildcards (like
/bin/*) in your Dispatcher filters. - Always sanitize input parameters using AEM's
XSSAPI. - If using
ResourceResolverFactory.getServiceResourceResolver(), ensure the sub-service has minimal read-only permissions on only the specific JCR tree needed.