This guide provides a structured approach to migrate from unsafe XStream usage to the secure Blocklist XStream adapter, minimizing disruption while maximizing security benefits.
The recommended migration follows a three-phase approach:
First, identify all XStream usage in your codebase:
1 2# Find direct XStream instantiation grep -r "new XStream" src/ grep -r "XStream.*=" src/ # Find XStream imports grep -r "import.*XStream" src/ # Find XML processing patterns grep -r "fromXML\|toXML" src/
Document each XStream usage by:
Prioritize migration based on risk:
Goal: Rapidly protect against known threats with minimal code changes
Replace existing XStream dependency:
1 2<!-- Remove or comment out existing XStream dependency --> <!-- <dependency> <groupId>com.thoughtworks</groupId> <artifactId>xstream</artifactId> <version>1.4.x</version> </dependency> --> <!-- Add blocklist adapter --> <dependency> <groupId>com.atlassian.security.serialblocklist</groupId> <artifactId>blocklist-xstream-adapter</artifactId> <version>${serialblocklist.version}</version> <scope>provided</scope> </dependency>
1 2XStream xstream = new XStream(); xstream.addPermission(AnyTypePermission.ANY);
1 2import com.atlassian.security.serialblocklist.xstream.BlocklistRestrictedXStream; XStream xstream = new BlocklistRestrictedXStream(); xstream.addPermission(AnyTypePermission.ANY); // Blocklist mode
If you use factory methods or dependency injection:
1 2@Bean public XStream xstream() { XStream xstream = new XStream(); xstream.addPermission(AnyTypePermission.ANY); return xstream; }
1 2@Bean public XStream xstream() { XStream xstream = new BlocklistRestrictedXStream(); xstream.addPermission(AnyTypePermission.ANY); return xstream; }
1 2@Test public void shouldBlockDangerousClasses() { XStream xstream = new BlocklistRestrictedXStream(); xstream.addPermission(AnyTypePermission.ANY); // This should throw ForbiddenClassException assertThrows(ForbiddenClassException.class, () -> { String maliciousXml = "<java.lang.ProcessBuilder>" + "<command><string>calc.exe</string></command>" + "</java.lang.ProcessBuilder>"; xstream.fromXML(maliciousXml); }); }
Goal: Understand actual type usage to prepare for allowlist mode
Create a wrapper to log type usage:
1 2public class MonitoringXStreamWrapper { private static final Logger logger = LoggerFactory.getLogger(MonitoringXStreamWrapper.class); private final XStream delegate; private final Set<String> observedTypes = ConcurrentHashMap.newKeySet(); public MonitoringXStreamWrapper(XStream delegate) { this.delegate = delegate; } public Object fromXML(String xml) { try { Object result = delegate.fromXML(xml); logTypeUsage(result); return result; } catch (Exception e) { logger.warn("XStream processing failed", e); throw e; } } private void logTypeUsage(Object obj) { if (obj != null) { String typeName = obj.getClass().getName(); if (observedTypes.add(typeName)) { logger.info("XStream type observed: {}", typeName); } } } public Set<String> getObservedTypes() { return new HashSet<>(observedTypes); } }
Deploy monitoring wrapper and collect data:
1 2// Wrap existing XStream instances XStream xstream = new BlocklistRestrictedXStream(); xstream.addPermission(AnyTypePermission.ANY); MonitoringXStreamWrapper wrapper = new MonitoringXStreamWrapper(xstream); // Use wrapper instead of direct XStream Object result = wrapper.fromXML(xmlData);
After some time of monitoring:
1 2// Export observed types Set<String> types = wrapper.getObservedTypes(); types.forEach(type -> System.out.println(type)); // Categorize types Set<String> applicationTypes = types.stream() .filter(type -> type.startsWith("com.yourcompany")) .collect(Collectors.toSet()); Set<String> frameworkTypes = types.stream() .filter(type -> type.startsWith("java.") || type.startsWith("org.")) .collect(Collectors.toSet());
Document the migration plan:
1 2// Essential application types Class<?>[] coreTypes = { UserProfile.class, ApplicationConfig.class, CacheEntry.class }; // Framework types needed Class<?>[] frameworkTypes = { ArrayList.class, HashMap.class, Date.class }; // Type hierarchies to allow Class<?>[] hierarchies = { Collection.class, // For lists, sets, etc. Map.class, // For various map implementations Number.class // For numeric types };
Goal: Implement zero-trust security with explicit type allowlisting
Based on Phase 2 analysis, create allowlist configurations:
1 2public class XStreamSecurityConfig { public static void configureAllowlist(XStream xstream) { // Core application types xstream.allowTypes(new Class<?>[] { UserProfile.class, UserPreferences.class, ApplicationConfig.class, DatabaseConfig.class, CacheEntry.class }); // Essential framework types xstream.allowTypes(new Class<?>[] { ArrayList.class, LinkedList.class, HashMap.class, LinkedHashMap.class, HashSet.class, Date.class, BigDecimal.class, UUID.class }); // Safe hierarchies xstream.allowTypeHierarchy(Collection.class); xstream.allowTypeHierarchy(Map.class); xstream.allowTypeHierarchy(Number.class); xstream.allowTypeHierarchy(Enum.class); } }
Migrate one service/component at a time:
1 2XStream xstream = new BlocklistRestrictedXStream(); xstream.addPermission(AnyTypePermission.ANY); // Permissive mode
1 2XStream xstream = new BlocklistRestrictedXStream(); // Remove AnyTypePermission.ANY - now in allowlist mode XStreamSecurityConfig.configureAllowlist(xstream);
Common issues and solutions:
ForbiddenClassException for legitimate type1 2// Solution: Add missing type to allowlist xstream.allowTypes(new Class<?>[] { MissingType.class });
1 2// Solution: Use hierarchy allowlisting carefully xstream.allowTypeHierarchy(CommonBaseClass.class);
1 2// Solution: Consider if these are really needed // If yes, use controlled hierarchy allowlisting // If no, refactor to use known types
1 2@Test public void shouldSerializeKnownTypes() { XStream xstream = new BlocklistRestrictedXStream(); XStreamSecurityConfig.configureAllowlist(xstream); UserProfile user = new UserProfile("john", "john@example.com"); String xml = xstream.toXML(user); UserProfile restored = (UserProfile) xstream.fromXML(xml); assertEquals(user.getUsername(), restored.getUsername()); }
1 2@Test public void shouldBlockUnknownTypes() { XStream xstream = new BlocklistRestrictedXStream(); XStreamSecurityConfig.configureAllowlist(xstream); // Should fail for types not in allowlist assertThrows(ForbiddenClassException.class, () -> { String xml = "<some.unknown.Class><field>value</field></some.unknown.Class>"; xstream.fromXML(xml); }); } @Test public void shouldBlockDangerousTypes() { XStream xstream = new BlocklistRestrictedXStream(); XStreamSecurityConfig.configureAllowlist(xstream); // Should fail for blocklisted types even if someone tries to allow them assertThrows(IllegalArgumentException.class, () -> { xstream.allowTypes(new Class<?>[] { ProcessBuilder.class }); }); }
If allowlist mode causes production issues:
1 2// Quick rollback - re-enable permissive mode XStream xstream = new BlocklistRestrictedXStream(); xstream.addPermission(AnyTypePermission.ANY); // Back to blocklist mode // Remove allowlist configuration temporarily
1 2// Periodic audit of allowed types public void auditAllowedTypes() { // Log current allowlist logger.info("Current allowed types: {}", getAllowedTypes()); // Check for unused types (requires monitoring) Set<String> unused = findUnusedAllowedTypes(); if (!unused.isEmpty()) { logger.warn("Potentially unused allowed types: {}", unused); } }
Migration is complete when:
BlocklistRestrictedXStreamAnyTypePermission.ANY in production (allowlist mode active)Rate this page: