Authorization Gaps Across the Web and EJB Tiers
A hospital scheduling application is packaged as an EAR with a JSF 2.2 web module and an EJB module. Roles are ADMIN, SCHEDULER, and CLERK; the app server maps LDAP groups to these role names. The login form posts to j_security_check over plain HTTP, and on success a custom bean calls request.login() and continues with the same JSESSIONID. Passwords are stored in the HR table as MD5(salt || password).
Two support tickets are open. First, a nightly @Schedule method on a @Singleton bean calls BillingEjb.recalculate(), which is annotated @RolesAllowed("ADMIN"); it fails every night with EJBAccessException, although an admin clicking the same action on a JSF page succeeds. Second, the mobile team called /api/patients directly with curl and received a full patient list without any credentials. The web.xml currently constrains /faces/* only, and the JSF pages hide admin buttons with rendered="#{user.isAdmin}".
1. Explain the nightly EJBAccessException and evaluate the ways to make the scheduled job work without weakening security for interactive users.
Read the worked answer
Why it fails. The EJB container performs the @RolesAllowed("ADMIN") check against the caller's java.security.Principal in the current security context. A @Schedule callback is invoked by the EJB timer service, not by a user: there is no authenticated caller, EJBContext.getCallerPrincipal() returns the container's anonymous principal, and isCallerInRole("ADMIN") is false. The check therefore throws EJBAccessException (wrapped by the container), and the timer transaction rolls back. The same method succeeds from JSF because the HTTP session carries an authenticated principal that is propagated to the local EJB call.
Options.
@RunAs("ADMIN")on the singleton that owns the@Schedulemethod. The container establishes a new role for calls the bean makes outbound, sorecalculate()sees a caller in role ADMIN. The role must be declared (deployment descriptor or a role mapping), and@RunAsdoes not fabricate a real user identity - audits will show the run-as principal, so log the job id explicitly. This is the standard answer.- Split the bean. Keep the secured
BillingEjbfor interactive use and expose an internal@PermitAll(or@RolesAllowed("SYSTEM")) facade used only by the scheduler, in a package not reachable from the web tier. - Do not schedule through the secured API at all. Move the schedule into a bean that calls an unsecured internal method, leaving
@RolesAllowedpurely on the boundary that humans reach.
What not to do. Do not delete @RolesAllowed from recalculate(), and do not have the job log in as an admin user with stored credentials. Both destroy the audit trail and turn a scheduling bug into a privilege-escalation path.
2. Assess the transport, session, and password-storage weaknesses and state the concrete configuration or API changes.
Read the worked answer
Transport. The login POST crosses the network in clear text; j_security_check credentials are recoverable by anyone on the path. Add a <security-constraint> covering the protected URL patterns with <user-data-constraint><transport-guarantee>CONFIDENTIAL</transport-guarantee></user-data-constraint>. The container then redirects http:// to https:// before the login form is served, so the form and the POST are both protected. INTEGRAL gives integrity without confidentiality and is not sufficient for credentials. Also mark the session cookie Secure and HttpOnly, and set an appropriate SameSite policy at the container or reverse proxy.
Session fixation. Because the application authenticates with request.login(), the pre-authentication session id may be retained; an attacker who planted a known JSESSIONID before login then owns the authenticated session. Call request.changeSessionId() immediately after a successful login() (Servlet 3.1) and invalidate the old session data, then re-establish only what the user needs. Also set a short session timeout and invalidate on logout.
Password storage. MD5(salt || password) is not acceptable: MD5 is fast and broken for collision/preimage use, and a single unsalted-speed hash is trivially brute-forced on a GPU. Migrate to a slow, memory-hard KDF - PBKDF2WithHmacSHA256 via SecretKeyFactory with a per-user random salt and at least 100,000 iterations (Java EE 7 runs on Java 7+, so PBKDF2WithHmacSHA1 is the guaranteed-available fallback), or bcrypt/scrypt/Argon2 through a vetted library. Store algorithm, iteration count, and salt alongside the digest so you can raise the cost later; rehash on next successful login. Prefer delegating to a container JAAS realm backed by the database so the application never handles the plaintext beyond the login request, and never log request parameters on the login path.
Finally, verify the whole flow with a scanner or a manual test that the plaintext http:// URL cannot reach the form.
3. Why is `rendered="#{user.isAdmin}"` irrelevant to the /api/patients exposure, and how do you close the gap with Servlet and EJB security?
Read the worked answer
UI hiding is not authorization. rendered is evaluated on the server while building the JSF response; when it is false, the component is simply not emitted. Nothing about the underlying endpoint changes. Any client that composes the request itself - curl, a mobile app, a scripted attacker - never sees the button and calls /api/patients directly. The response confirms there is no <security-constraint> for /api/*: the container sees no constraint, treats the URL as unprotected, does not authenticate, and the JAX-RS resource runs with an anonymous caller. The same reasoning applies to disabled buttons and JavaScript checks.
Closing the gap.
- Add a
<security-constraint>for/api/*with a<web-resource-collection>naming the URL pattern and, ideally, explicit<http-method>entries, plus an<auth-constraint>listing the permitted roles (for example SCHEDULER, ADMIN) and a<security-role>declaration that matches the LDAP group mapping exactly - role names are case-sensitive strings. - Choose a
<login-config>appropriate to machine clients.BASICover HTTPS is simplest for the mobile team;FORMis for browsers. Do not mix: a browser-facing FORM login on/api/*returns an HTML login page to a JSON client. - Set
<deny-uncovered-http-methods>true</deny-uncovered-http-methods>(Servlet 3.1) so any HTTP method not explicitly listed in the constraint is denied rather than allowed. Without it, a constraint listing only GET leaves POST/PUT/DELETE unprotected - the classic HTTP method bypass. - Keep
@RolesAllowedon the EJB/backing methods as defense in depth: the web constraint protects the URL, the EJB annotation protects the business operation regardless of entry point. They are independent checks, and both must be configured; neither substitutes for the other. - Audit the deployment descriptor for other unconstrained paths (
/faces/*is covered, but JSF resource handling,/javax.faces.resource/*, and any servlet mapped outside those patterns are not).
Test. Unauthenticated curl must return 401 (not the patient list); authenticated as CLERK must return 403; as SCHEDULER must return 200.