Day 32 of 40
Servlets, CSRF and CORS
Registering by resource type, and the request security model
By the end of today you should be able to
- Register a servlet by resource type and explain why that beats registering by path
- Explain AEM's CSRF protection and when it applies
- Configure CORS correctly without opening the site up
- Know when a custom JCR namespace is warranted
Servlets, and the right way to bind them
Sling offers two ways to register a servlet, and one of them is a security problem.
By resource type — preferred:
@Component(service = Servlet.class)
@SlingServletResourceTypes(
resourceTypes = "wknd/components/page",
selectors = "adventures",
extensions = "json",
methods = HttpConstants.METHOD_GET)
public class AdventuresServlet extends SlingSafeMethodsServlet {
@Override
protected void doGet(SlingHttpServletRequest req,
SlingHttpServletResponse resp) { ... }
}
That responds to /content/wknd/us/en/home.adventures.json.
By path — avoid:
@SlingServletPaths("/bin/wknd/adventures")
A path-bound servlet is reachable regardless of the ACLs on any content, so it sidesteps the repository's permission model entirely — you must reimplement authorisation yourself, and it is easy to forget. Resource-type binding means the user must already be able to read the resource. Path-bound servlets under /bin also need explicit Dispatcher filter handling.
Extend SlingSafeMethodsServlet for read-only servlets and
SlingAllMethodsServlet when you genuinely need POST. The first makes the intent explicit
and removes a whole class of mistakes.
CSRF protection
AEM ships a CSRF filter that requires a token on state-changing requests
(POST, PUT, DELETE) to author. The token comes from
/libs/granite/csrf/token.json, and Granite UI adds it automatically to forms and AJAX
calls made through its framework.
You meet it when you write a custom POST outside that framework:
const { token } = await fetch('/libs/granite/csrf/token.json')
.then(r => r.json());
await fetch('/bin/wknd/save', {
method: 'POST',
headers: { 'CSRF-Token': token },
body: formData
});
Paths can be excluded from the filter via OSGi configuration, but doing so removes the protection for those paths — legitimate only for endpoints that are genuinely safe to call cross-origin, and worth a comment explaining why.
CORS
Cross-origin requests are blocked by the browser unless the server opts in. In AEM this is the Adobe Granite Cross-Origin Resource Sharing Policy, a factory configuration — you create one per policy:
{
"alloworigin": ["https://app.example.com"],
"allowedpaths": ["/content/wknd/.*\\.model\\.json"],
"supportedmethods": ["GET", "HEAD"],
"supportscredentials": false
}
- Never use
*foralloworiginin production. List the origins you actually serve. - Scope
allowedpathstightly — the specific endpoints, not the whole site. supportscredentials: truepermits cookies to be sent cross-origin. Combined with a wildcard origin it is a serious vulnerability, and browsers reject that combination for good reason.
Custom namespaces
JCR property and node names are namespaced: jcr:, sling:, cq:,
dam:. You can register your own — wknd:campaignCode — via a namespace
registration in your project.
It is rarely necessary. A plain unnamespaced property name is fine for component content, and a custom namespace adds registration and migration overhead. It earns its place when you are defining a genuinely new content model that must not collide with product properties, particularly if you also define custom node types.
Bind servlets by resource type so the repository keeps enforcing permissions. Respect CSRF rather than excluding paths. Scope CORS to real origins and real paths. Reach for a custom namespace only when you are defining a real content model.
Watch
Adobe's own videos for this topic. They load only when you press play.
Read on Experience League
The primary sources these notes are drawn from.
Your notes
Saved automatically to this browser.
Check yourself
10 questions on today's material. 80% to pass.