Back to Sealed Classes & Interfaces
Q10Question 10 of 12

Can you break the sealed contract via reflection or bytecode manipulation?

Key Concepts
  • Reflection cannot break sealing because the JVM re-checks the `PermittedSubclasses` attribute at class-load time.
  • Attempting to define an unlisted subclass at runtime throws `IncompatibleClassChangeError` at load time.
  • `Proxy.newProxyInstance` cannot implement a sealed interface because the proxy class is not in the permits list.
  • Raw bytecode manipulation can forge the `PermittedSubclasses` attribute because sealing is a class-file integrity rule, not a security sandbox.
  • Sealing is for API design and compiler-enforced exhaustiveness, not for defending against attackers who control class files.
  • Production defense against tampered bytecode comes from signed JARs, immutable containers, and module strong encapsulation.
  • The JVM validates at load time that each permitted subtype is a genuine direct subclass in the same module.

Definition

Reflection cannot break sealing because the JVM re-checks the permits list at load time, but raw bytecode edits can forge that metadata, so sealing is an integrity guarantee, not a security sandbox.
  • JVM verification – at class-load time, the JVM validates that any class extending a sealed type appears in its PermittedSubclasses attribute and is in the same module.
  • Reflection blocked – Proxy.newProxyInstance and runtime code generation cannot bypass this check; an unlisted class throws IncompatibleClassChangeError.
  • Bytecode forgery – raw bytecode tools (ASM, ByteBuddy) can rewrite the PermittedSubclasses attribute; the JVM checks consistency against whatever attribute is present.
  • Integrity, not security – sealing is a class-file integrity constraint, not a security boundary; it holds as long as the class files are trustworthy.
  • Production defense – signed JARs, immutable containers, and JPMS strong encapsulation protect against tampered bytecode.

The sealed permits list is like a vault's approved-signatory list that the security system checks every time someone tries to enter. If Rajesh is not on the list, the guard turns him away at load time: no polite reflection trick can sneak him in. But if a malicious insider literally rewrites the official signatory document (edits the raw bytecode), the guard will honor the forged list. The seal protects against accidents and honest mistakes, not against someone who controls the class files themselves.

Understanding the Concepts

How does the JVM enforce sealing at load time?

When you compile a sealed class, the compiler writes a PermittedSubclasses attribute listing the allowed subtypes. The JVM verifier checks at class loading that any class declaring a sealed type as its superclass appears in that list and is in the same module. If the check fails, loading throws IncompatibleClassChangeError.

JAVA
package com.bank.model;

import java.math.BigDecimal;

public sealed interface Account
        permits SavingsAccount, CurrentAccount {
    BigDecimal balance();
}

final record SavingsAccount(BigDecimal balance) implements Account {}
final record CurrentAccount(BigDecimal balance) implements Account {}

// If someone tries to load a rogue class:
// class RogueAccount implements Account { ... }
// JVM throws: IncompatibleClassChangeError
//   "RogueAccount is not permitted to implement Account"

Can reflection bypass the sealed contract?

No. Reflection does not give you a backdoor. The load-time verification still applies.

JAVA
package com.bank.service;

import java.lang.reflect.Proxy;
import java.math.BigDecimal;

public class ReflectionAttempt {

    sealed interface Account permits SavingsAccount {
        BigDecimal balance();
    }
    final record SavingsAccount(BigDecimal balance) implements Account {}

    public static void main(String[] args) {
        try {
            // Proxy cannot implement a sealed interface
            Account proxy = (Account) Proxy.newProxyInstance(
                    Account.class.getClassLoader(),
                    new Class<?>[]{ Account.class },
                    (p, method, a) -> new BigDecimal("999999")
            );
        } catch (Exception e) {
            System.out.println("Proxy failed: " + e.getClass().getSimpleName());
            System.out.println("Message: " + e.getMessage());
        }
    }
}
TEXT
Proxy failed: IllegalArgumentException
Message: Account is sealed and does not permit the proxy class

Can runtime code generation bypass sealing?

No. Even bytecode generation libraries like ByteBuddy or ASM that generate a subclass at runtime still have to pass the same load-time verification. The generated class is rejected unless the sealed parent lists it.

JAVA
package com.bank.service;

public class ByteCodeGenAttempt {
    // Attempting with ByteBuddy:
    // new ByteBuddy()
    //     .subclass(SavingsAccount.class)  // SavingsAccount is final record
    //     .make()
    //     .load(getClassLoader())
    //
    // Result: IllegalArgumentException or IncompatibleClassChangeError
    // because SavingsAccount is final and not in any permits list
}

How can bytecode manipulation break sealing?

The sealed guarantee is encoded entirely in the PermittedSubclasses attribute. If an attacker can edit that attribute to add a name, strip it, or replace the class file with a modified version, the JVM will accept the forged metadata.

JAVA
package com.bank.service;

public class BytecodeForgeryExplanation {
    // The PermittedSubclasses attribute in Account.class:
    // [SavingsAccount, CurrentAccount]
    //
    // If an attacker uses ASM to modify this to:
    // [SavingsAccount, CurrentAccount, RogueAccount]
    // and loads the modified class file:
    // The JVM will accept RogueAccount as permitted.
    //
    // Or if the attacker strips the attribute entirely:
    // The class is no longer sealed at runtime.
    //
    // This requires control over the class files loaded by the JVM.
}

Why is sealing not a security boundary?

Sealing is a class-file integrity constraint, not a security sandbox. It prevents accidental or reflective subclassing, but it does not defend against an attacker who controls the deployment artifacts. Security against tampered bytecode requires separate layers.

| Threat | Sealing Protects? | Defense |
|---|---|---|
| Accidental subclass in source code | Yes (compile error) | Sealed permits |
| Reflective subclass (Proxy, etc.) | Yes (load-time check) | JVM verification |
| Runtime code generation (ByteBuddy) | Yes (load-time check) | JVM verification |
| Modified class file on disk | No | Signed JARs, immutable containers |
| Replaced class file in classloader | No | Module encapsulation, code signing |

How do you protect sealed hierarchies in production?

In a banking deployment, pair sealing with artifact integrity measures.

JAVA
package com.bank.deployment;

public class ProductionSecurity {
    // Layer 1: sealed permits → compile-time closed set
    // Layer 2: JVM verification → load-time check
    // Layer 3: signed JARs (jarsigner) → integrity verification
    // Layer 4: immutable container images → no runtime modification
    // Layer 5: JPMS strong encapsulation → no unauthorized access
    // Layer 6: no loading untrusted classes → class loader policy
}
BASH
# Sign a JAR for integrity verification
jarsigner -keystore national-bank.jks \
          -storepass changeit \
          bank-accounts.jar \
          nbi-signing-key

# Verify at deployment
jarsigner -verify bank-accounts.jar

What does IncompatibleClassChangeError look like?

This is the error thrown when the JVM detects an unlisted subclass at load time.

JAVA
package com.bank.service;

public class ErrorDemo {
    // If a class RogueAccount that implements Account
    // is loaded without being in Account's PermittedSubclasses:
    //
    // Exception in thread "main"
    //   java.lang.IncompatibleClassChangeError:
    //   class RogueAccount cannot implement sealed interface Account
    //   because it is not a permitted subclass
}

What should a senior engineer know?

The precise answer in an interview is: reflection cannot break sealing because the JVM checks the PermittedSubclasses attribute at load time, but bytecode forgery can, because the guarantee is only as trustworthy as the class files the JVM loads. At National Bank of India, Pankaj pairs sealing with signed JARs and immutable container images to get both the design benefit (exhaustive switches, closed API) and genuine tamper resistance.

Visual Overview

DIAGRAM
┌─────────────────────────────────────────────┐
│  Sealed Contract Enforcement                │
├─────────────────────────────────────────────┤
│                                             │
│  Compile Time                               │
│  ┌────────────────────────┐                 │
│  │ Compiler checks permits│                 │
│  │ Writes PermittedSub-   │                 │
│  │ classes attribute       │                 │
│  └──────────┬─────────────┘                 │
│             ▼                               │
│  Load Time (JVM Verifier)                   │
│  ┌────────────────────────┐                 │
│  │ Check: is subclass in  │                 │
│  │ PermittedSubclasses?   │                 │
│  │ Check: same module?    │                 │
│  └──────────┬─────────────┘                 │
│             │                               │
│    ┌────────┴────────┐                      │
│    │                 │                      │
│    ▼                 ▼                      │
│  Listed: OK      Not listed:                │
│                  Incompatible               │
│                  ClassChangeError            │
│                                             │
│  Bytecode Forgery (outside JVM control)     │
│  ┌────────────────────────┐                 │
│  │ Modify/strip the       │                 │
│  │ PermittedSubclasses    │                 │
│  │ attribute in .class    │                 │
│  │ file → JVM accepts     │                 │
│  │ forged metadata        │                 │
│  └────────────────────────┘                 │
│  Defense: signed JARs, immutable containers │
└─────────────────────────────────────────────┘

Key Takeaway

You cannot break the sealed contract through legitimate reflection because sealing is re-verified by the JVM at class-load time. If any class tries to extend a sealed type without being on its permits list, the verifier throws IncompatibleClassChangeError. Proxy, ByteBuddy, and other runtime code generators all pass through the same verification, so none of them provide a backdoor.

The honest answer, though, is that sealing is an integrity guarantee, not a security boundary. The entire guarantee lives in the PermittedSubclasses attribute of the parent class file. If an attacker can rewrite that attribute with a tool like ASM, or replace the class file entirely, the JVM will accept the forged metadata because it only checks consistency against whatever attribute is present.

In a banking project at National Bank of India, Pankaj seals the account-product hierarchy to get compiler-enforced exhaustiveness and a clean, closed API. He relies on the JVM verifier to stop any accidental or reflective attempt to introduce an unapproved product. But he never treats sealing as his defense against a malicious party who controls the deployment. For that he relies on signed JARs, immutable container images, and module strong encapsulation.

The precise interview answer is: reflection cannot break sealing because the JVM checks the permits list at load time, but bytecode forgery can, because the guarantee is only as trustworthy as the class files the JVM loads. That is why you pair sealing with artifact signing and immutable deployments.

Remember one sentence: reflection cannot break sealing because the JVM validates the PermittedSubclasses attribute at load time, but raw bytecode manipulation can forge that metadata, so sealing is an integrity guarantee, not a security sandbox.

---

Share
Use ← → arrow keys to navigate