Goal: Understand actual type usage to prepare for allowlist mode
Timeline: 2-4 weeks
Security improvement: Maintain current protection while gathering data
This phase focuses on analyzing your application's actual XStream usage patterns to prepare for the final migration to allowlist mode.
Create a wrapper to log type usage patterns:
1 2public class TypeUsageMonitoringWrapper { private static final Logger logger = LoggerFactory.getLogger(TypeUsageMonitoringWrapper.class); private final XStream delegate; private final Set<String> observedTypes = ConcurrentHashMap.newKeySet(); private final AtomicLong totalOperations = new AtomicLong(0); public TypeUsageMonitoringWrapper(XStream delegate) { this.delegate = delegate; } public Object fromXML(String xml) { totalOperations.incrementAndGet(); try { Object result = delegate.fromXML(xml); logTypeUsage(result); return result; } catch (Exception e) { logger.warn("XStream processing failed", e); throw e; } } public String toXML(Object obj) { totalOperations.incrementAndGet(); if (obj != null) { logTypeUsage(obj); } return delegate.toXML(obj); } private void logTypeUsage(Object obj) { if (obj != null) { String typeName = obj.getClass().getName(); if (observedTypes.add(typeName)) { logger.info("XStream type observed: {} (total operations: {})", typeName, totalOperations.get()); } // Also track nested types analyzeNestedTypes(obj); } } private void analyzeNestedTypes(Object obj) { // Analyze collections, maps, and other complex structures if (obj instanceof Collection) { ((Collection<?>) obj).forEach(this::logTypeUsage); } else if (obj instanceof Map) { Map<?, ?> map = (Map<?, ?>) obj; map.keySet().forEach(this::logTypeUsage); map.values().forEach(this::logTypeUsage); } } public Set<String> getObservedTypes() { return new HashSet<>(observedTypes); } public long getTotalOperations() { return totalOperations.get(); } }
1 2@Configuration @ConditionalOnProperty("migration.phase2.monitoring.enabled") public class Phase2MonitoringConfig { @Bean @Primary public XStream monitoringXStream(@Qualifier("coreXStream") XStream coreXStream) { return new TypeUsageMonitoringWrapper(coreXStream); } @Bean("coreXStream") public XStream coreXStream() { XStream xstream = new BlocklistRestrictedXStream(); xstream.addPermission(AnyTypePermission.ANY); // Still in blocklist mode // Apply existing configuration return xstream; } }
1 2// Wrap existing XStream instances XStream coreXStream = new BlocklistRestrictedXStream(); coreXStream.addPermission(AnyTypePermission.ANY); TypeUsageMonitoringWrapper monitoringWrapper = new TypeUsageMonitoringWrapper(coreXStream); // Use wrapper instead of direct XStream Object result = monitoringWrapper.fromXML(xmlData); String xml = monitoringWrapper.toXML(dataObject);
1 2@Component @ConditionalOnProperty("migration.phase2.monitoring.enabled") public class TypeUsageCollectionService { private final TypeUsageMonitoringWrapper monitoringWrapper; private final ObjectMapper objectMapper; @Scheduled(fixedRate = 300000) // Every 5 minutes public void collectUsageData() { UsageSnapshot snapshot = UsageSnapshot.builder() .timestamp(Instant.now()) .observedTypes(monitoringWrapper.getObservedTypes()) .totalOperations(monitoringWrapper.getTotalOperations()) .build(); // Store for analysis storeSnapshot(snapshot); // Log summary logger.info("Phase 2 monitoring - Types observed: {}, Operations: {}", snapshot.getObservedTypes().size(), snapshot.getTotalOperations()); } private void storeSnapshot(UsageSnapshot snapshot) { try { String json = objectMapper.writeValueAsString(snapshot); Files.write( Paths.get("monitoring/usage-" + snapshot.getTimestamp() + ".json"), json.getBytes(), StandardOpenOption.CREATE ); } catch (Exception e) { logger.error("Failed to store usage snapshot", e); } } }
1 2public class UsageAnalysisTools { public TypeAnalysisReport analyzeCollectedData(List<UsageSnapshot> snapshots) { Set<String> allObservedTypes = snapshots.stream() .flatMap(snapshot -> snapshot.getObservedTypes().stream()) .collect(Collectors.toSet()); // Categorize types Set<String> applicationTypes = allObservedTypes.stream() .filter(type -> type.startsWith("com.yourcompany")) .collect(Collectors.toSet()); Set<String> frameworkTypes = allObservedTypes.stream() .filter(type -> type.startsWith("java.") || type.startsWith("org.") || type.startsWith("javax.")) .collect(Collectors.toSet()); Set<String> thirdPartyTypes = allObservedTypes.stream() .filter(type -> !applicationTypes.contains(type) && !frameworkTypes.contains(type)) .collect(Collectors.toSet()); return TypeAnalysisReport.builder() .totalUniqueTypes(allObservedTypes.size()) .applicationTypes(applicationTypes) .frameworkTypes(frameworkTypes) .thirdPartyTypes(thirdPartyTypes) .recommendations(generateRecommendations(applicationTypes, frameworkTypes)) .build(); } private List<String> generateRecommendations(Set<String> appTypes, Set<String> frameworkTypes) { List<String> recommendations = new ArrayList<>(); if (appTypes.size() < 20) { recommendations.add("LOW_COMPLEXITY: Allowlist migration should be straightforward"); } else if (appTypes.size() < 50) { recommendations.add("MEDIUM_COMPLEXITY: Plan careful allowlist migration"); } else { recommendations.add("HIGH_COMPLEXITY: Consider gradual allowlist migration"); } if (frameworkTypes.size() > 100) { recommendations.add("MANY_FRAMEWORK_TYPES: Consider using hierarchy allowlisting"); } return recommendations; } }
1 2@RestController @ConditionalOnProperty("migration.phase2.monitoring.enabled") public class MigrationDataController { private final TypeUsageMonitoringWrapper monitoringWrapper; private final UsageAnalysisTools analysisTools; @GetMapping("/migration/phase2/observed-types") public ResponseEntity<Set<String>> getObservedTypes() { return ResponseEntity.ok(monitoringWrapper.getObservedTypes()); } @GetMapping("/migration/phase2/analysis-report") public ResponseEntity<TypeAnalysisReport> getAnalysisReport() { List<UsageSnapshot> snapshots = loadStoredSnapshots(); TypeAnalysisReport report = analysisTools.analyzeCollectedData(snapshots); return ResponseEntity.ok(report); } @GetMapping("/migration/phase2/allowlist-config") public ResponseEntity<AllowlistConfig> generateAllowlistConfig() { Set<String> observedTypes = monitoringWrapper.getObservedTypes(); AllowlistConfig config = createAllowlistConfig(observedTypes); return ResponseEntity.ok(config); } }
After 1-2 weeks of monitoring, analyze the data:
1 2// Export observed types Set<String> types = monitoringWrapper.getObservedTypes(); types.forEach(System.out::println); // Categorize types Set<String> applicationTypes = types.stream() .filter(type -> type.startsWith("com.yourcompany")) .collect(Collectors.toSet()); Set<String> javaTypes = types.stream() .filter(type -> type.startsWith("java.")) .collect(Collectors.toSet()); System.out.println("Application types: " + applicationTypes.size()); System.out.println("Java standard types: " + javaTypes.size());
1 2public class AllowlistConfigGenerator { public AllowlistConfiguration generateConfig(Set<String> observedTypes) { AllowlistConfiguration config = new AllowlistConfiguration(); // Essential application types Set<Class<?>> coreTypes = observedTypes.stream() .filter(type -> type.startsWith("com.yourcompany.core")) .map(this::safeLoadClass) .filter(Objects::nonNull) .collect(Collectors.toSet()); // Framework types that should use hierarchies Set<Class<?>> hierarchyTypes = identifyHierarchyTypes(observedTypes); // Individual types that need explicit allowlisting Set<Class<?>> explicitTypes = observedTypes.stream() .map(this::safeLoadClass) .filter(Objects::nonNull) .filter(clazz -> !shouldUseHierarchy(clazz)) .collect(Collectors.toSet()); config.setCoreTypes(coreTypes); config.setHierarchyTypes(hierarchyTypes); config.setExplicitTypes(explicitTypes); return config; } private Class<?> safeLoadClass(String className) { try { return Class.forName(className); } catch (ClassNotFoundException e) { logger.warn("Could not load class for allowlist: {}", className); return null; } } private boolean shouldUseHierarchy(Class<?> clazz) { return Collection.class.isAssignableFrom(clazz) || Map.class.isAssignableFrom(clazz) || Number.class.isAssignableFrom(clazz); } }
1 2/** * Phase 3 Migration Plan * Generated from Phase 2 analysis * * Analysis Period: 2024-12-01 to 2024-12-15 * Total Operations: 45,678 * Unique Types: 127 * * Type Breakdown: * - Application types: 23 * - Framework types: 89 * - Third-party types: 15 * * Migration Complexity: MEDIUM * Estimated Phase 3 Duration: 2-3 weeks */ public class Phase3MigrationPlan { // Essential application types (must allowlist) private static final Class<?>[] CORE_TYPES = { UserProfile.class, ApplicationConfig.class, SessionData.class, // ... identified from analysis }; // Framework hierarchies (safe to allowlist broadly) private static final Class<?>[] HIERARCHY_TYPES = { Collection.class, Map.class, Number.class, // ... identified from analysis }; // Individual framework types (explicit allowlist) private static final Class<?>[] FRAMEWORK_TYPES = { ArrayList.class, HashMap.class, Date.class, // ... identified from analysis }; }
1 2public class MigrationRiskAssessment { public RiskAssessment assessMigrationRisk(Set<String> observedTypes) { RiskAssessment assessment = new RiskAssessment(); // Check for problematic patterns long dynamicTypes = observedTypes.stream() .filter(type -> type.contains("$Proxy")) .count(); long unknownThirdParty = observedTypes.stream() .filter(type -> !type.startsWith("java.") && !type.startsWith("com.yourcompany")) .count(); // Assess risk levels if (dynamicTypes > 10) { assessment.addRisk("HIGH", "Many dynamic proxy types detected"); } if (unknownThirdParty > 20) { assessment.addRisk("MEDIUM", "Many third-party types in use"); } if (observedTypes.size() > 100) { assessment.addRisk("MEDIUM", "Large number of types may complicate allowlist"); } return assessment; } }
1 2@TestConfiguration public class Phase3TestConfig { @Bean @Primary public XStream testAllowlistXStream() { XStream xstream = new BlocklistRestrictedXStream(); // Note: NO AnyTypePermission.ANY - this is allowlist mode // Configure based on Phase 2 analysis configureFromAnalysis(xstream); return xstream; } private void configureFromAnalysis(XStream xstream) { // Load configuration generated from Phase 2 data AllowlistConfiguration config = loadPhase2Configuration(); xstream.allowTypes(config.getCoreTypes().toArray(new Class[0])); xstream.allowTypes(config.getFrameworkTypes().toArray(new Class[0])); for (Class<?> hierarchyType : config.getHierarchyTypes()) { xstream.allowTypeHierarchy(hierarchyType); } } }
✅ Phase 2 is complete when:
After completing Phase 2:
Phase 2 provides the crucial data needed to make Phase 3 migration smooth and successful.
Rate this page: