skip to content

What is a ProtectionDomain, and how do the permissions of code on the call stack get combined during an access check?

level: seniorimportance: should knowfreq 22%

answer

  1. ProtectionDomain = CodeSource + granted permissions
  2. Check walks the WHOLE stack
  3. Effective perms = intersection (weakest frame wins)
  4. doPrivileged stops the walk at that frame
  5. Prevents confused-deputy / privilege laundering

basics

~20 s

A ProtectionDomain groups code from the same source with the permissions granted to it. When code is checked, Java looks at every method on the call stack and only allows the action if every one of them has the permission — the most restrictive caller wins.

solid answer

~50 s

A ProtectionDomain associates a CodeSource (where code came from: URL + signers) with the set of Permissions the policy grants it. Every class belongs to one protection domain. The crucial rule is how permissions combine across a call stack: when AccessController.checkPermission runs, it walks every frame from the current method down to the caller and requires that *every* protection domain on the stack implies the requested permission. Effectively the permissions are intersected — the least-privileged frame caps the whole call. This is the 'a chain is only as strong as its weakest link' rule, and it stops trusted code from being tricked into doing something on behalf of untrusted code. The escape hatch is AccessController.doPrivileged: it tells the walk to stop at the privileged frame, so a trusted library can perform a sensitive op even when an untrusted caller is higher up the stack — but only using the library's own permissions.

code

java · 15 lines
java
// Trusted library reading its OWN config despite an untrusted caller on the stack.
Properties cfg = AccessController.doPrivileged(
    (PrivilegedAction<Properties>) () -> {
        Properties p = new Properties();
        try (InputStream in = new FileInputStream("/etc/mylib/config.properties")) {
            p.load(in);              // checked with THIS frame's permissions only
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
        return p;
    });

// DANGER: never pass a caller-supplied path into the privileged block —
// that turns the library into a confused deputy that reads any file for the caller.
// (All of this is part of the SecurityManager model, deprecated for removal per JEP 411.)

go deeper

for a junior

Can say each class has a domain tying its origin to what it may do, and an action needs all callers to be allowed.

for a middle

Defines ProtectionDomain = CodeSource + permissions, and knows the stack is walked and any disallowed frame fails the check.

for a senior

Explains the intersection semantics precisely, why it prevents confused-deputy attacks, and how doPrivileged narrows the walk to its own frame.

for a principal

Discusses the security implications of doPrivileged misuse (tainted input, over-broad blocks), the threat model it was built for, and why the enforcement layer is being removed despite the concept being sound.

## Building blocks **CodeSource** = where a class came from: a code-location URL plus the certificate chain of whoever signed it (may be none). Two classes from the same JAR signed by the same key share a CodeSource. **ProtectionDomain** = a CodeSource paired with the **PermissionCollection** the policy grants it (and, for JAAS, the authenticated Principals). Every loaded class is stamped with exactly one ProtectionDomain by its classloader. Think of it as the answer to "who is this code and what is it allowed to do?". ## Why combine across the stack at all Imagine untrusted plugin code calls a trusted utility method that opens a file. If the check only looked at the *immediately running* method (the trusted utility), the untrusted plugin could launder any operation through trusted code. To prevent this **confused-deputy** attack, the check considers the *whole chain of callers*. ## The intersection rule When `AccessController.checkPermission(perm)` runs, it: 1. Captures the current stack of frames. 2. For each frame, finds the class's ProtectionDomain. 3. Asks each domain: *do you imply `perm`?* 4. **Succeeds only if every domain says yes.** If any single frame lacks the permission, an `AccessControlException` is thrown. So the effective permission set of a call is the **intersection** of all the domains on the stack — the *least* privileged frame is the ceiling. Adding a more-trusted caller never grants more; adding a less-trusted caller can only take away. ## doPrivileged — the deliberate escape hatch Sometimes a trusted library legitimately needs to do something its (untrusted) caller cannot — e.g. a logging framework reading its own config file. `AccessController.doPrivileged(PrivilegedAction)` marks the current frame as privileged: the stack walk **stops** at that frame and does *not* inspect callers above it. The library then acts with its *own* domain's permissions only. This must be used carefully — wrapping the wrong thing, or passing tainted input (like a caller-supplied path) into a doPrivileged block, reintroduces the confused-deputy hole. ## A worked example Stack (top = currently running): - Frame A: trusted lib, granted FilePermission "/etc/*","read" - Frame B: app code, granted FilePermission "/etc/*","read" - Frame C: plugin code, granted nothing A read of /etc/passwd is requested. The walk checks A (ok), B (ok), C (no) → **denied**, because C's domain doesn't imply it. If A had wrapped the read in `doPrivileged`, the walk would stop at A and **allow** it using A's permissions, ignoring B and C. ## Status Protection domains are still a real concept in the class-loading internals, but the *enforcement* (SecurityManager + AccessController checks) is deprecated for removal (JEP 411). Treat the stack-intersection / doPrivileged semantics as legacy knowledge: vital for reading old code and understanding why a sandbox behaves as it does, not for new design.

  • Why does the access check intersect permissions across the stack instead of trusting the running method?
    To stop a confused-deputy attack: if only the running (trusted) frame were checked, untrusted code could call trusted methods to perform actions it isn't allowed to do itself. Requiring every frame to hold the permission closes that laundering path.
  • What does AccessController.doPrivileged change about the stack walk?
    It marks the current frame as privileged so the walk stops there and does not inspect callers above it; the sensitive action then runs with that frame's own permissions only.

saying these in an interview costs you the question

  • Saying the check only looks at the currently running method — it walks all callers on the stack.
  • Claiming permissions are unioned across the stack — they are intersected; the least-privileged frame caps it.
  • Thinking doPrivileged grants the caller's permissions — it uses the privileged frame's own permissions and ignores callers above.
  • Passing untrusted, caller-controlled input into a doPrivileged block (reopens the confused-deputy hole).

context