* WW-5626 add ParameterAuthorizer#resolveTarget for centralized ModelDriven resolution
Move the ValueStack peek logic that derives the target object from action+ModelDriven
state out of ParametersInterceptor and into ParameterAuthorizer. Callers that need
both authorization and the resolved target (for downstream OGNL allowlisting) can
now call resolveTarget once and reuse the result.
* WW-5626 delegate ModelDriven target resolution to ParameterAuthorizer
Replace the inline ValueStack peek in ParametersInterceptor#isParameterAnnotatedAndAllowlist
with a call to ParameterAuthorizer#resolveTarget. The ModelDriven import is no longer
needed in this class.
* WW-5626 defensively skip non-String JSON keys in authorization filter
The (String) cast in filterUnauthorizedKeysRecursive threw ClassCastException
for any custom JSONReader producing non-String keys. Replace with an instanceof
pattern that debug-logs and skips entries whose key cannot be converted to a
parameter path.
* WW-5626 add real JacksonJsonHandler integration tests for @StrutsParameter filtering
The existing ContentTypeInterceptorTest uses mock ContentTypeHandlers, so its
requireAnnotations=true tests verify only that intercept() returns SUCCESS — they
assert nothing about which properties were actually filtered. These integration
tests use a real JacksonJsonHandler + a real StrutsParameterAuthorizer to verify
end-to-end property-level filtering for top-level annotated/unannotated properties
and nested properties at varying authorized depths.
The SecureRestAction fixture documents a semantic divergence: REST's recursive
copy authorizes each path level independently, so depth-0 authorization on the
top-level property requires @StrutsParameter on the setter even when nested
field access is the actual goal. ParametersInterceptor only requires the getter
annotation. This divergence is tracked for the Approach C refactor.
* WW-5626 make ParameterAuthorizer#resolveTarget a default method to preserve SAM
Making resolveTarget abstract broke ParameterAuthorizer as a functional interface,
which the existing JSON and REST plugin tests rely on for lambda-based stubs:
interceptor.setParameterAuthorizer((parameterName, target, action) -> true);
The default returns the action unchanged — adequate for lambda test stubs whose
authorization decisions don't depend on the resolved target. The production
implementation (StrutsParameterAuthorizer) overrides this with the proper
ModelDriven value-stack peek.
* WW-5624 fix(security): enforce @StrutsParameter on JSON/REST body deserialization
Extract ParameterAuthorizer service from ParametersInterceptor to share
@StrutsParameter annotation enforcement across all input channels.
The json-plugin (JSONInterceptor) and rest-plugin (ContentTypeInterceptor)
previously bypassed @StrutsParameter checks when deserializing request
bodies, allowing mass assignment even when
struts.parameters.requireAnnotations=true.
Changes:
- New ParameterAuthorizer interface and DefaultParameterAuthorizer impl
- JSONInterceptor: filter unauthorized Map keys before populateObject()
- ContentTypeInterceptor: two-phase deserialization (fresh instance then
copy authorized properties) when requireAnnotations=true; direct
deserialization for backward compat when disabled
- OGNL ThreadAllowlist side effects remain in ParametersInterceptor only
- Full DI wiring: struts-beans.xml + StrutsBeanSelectionProvider +
DefaultConfiguration
- 15 new unit tests for ParameterAuthorizer, 2 for JSON plugin,
2 for REST plugin; 32 existing regression tests verified
* WW-5624 address review feedback from lukaszlenart on PR #1657
1. Rename DefaultParameterAuthorizer → StrutsParameterAuthorizer
per Struts naming convention (inline suggestion)
2. Narrow ModelDriven exemption: require action instanceof ModelDriven
before exempting target from @StrutsParameter checks. Prevents
non-ModelDriven root objects (e.g. JSONInterceptor.root) from
bypassing annotation enforcement.
3. Recursive JSON key filtering: filterUnauthorizedKeys() now recurses
into nested Maps and Lists, building dot-notation paths (e.g.
"address.city") for path-aware @StrutsParameter(depth=N) checks.
4. Deep REST property copy: copyAuthorizedProperties() now recurses
into nested bean types with path-aware authorization. Collections,
Maps, primitives, and java.lang/java.time types are copied directly.
5. Null-skip semantics preserved and documented: in two-phase
deserialization, null in freshInstance is indistinguishable from
"not present in request" — clearing would destroy pre-initialized
fields. Kept as intentional design choice with inline documentation.
6. No-arg constructor fallback: when target class lacks a no-arg
constructor, falls back to single-phase deserialization with
post-scrub of unauthorized properties, preserving backward compat.
7. New regression tests:
- Non-ModelDriven target with different object (must not exempt)
- Nested JSON keys recursively filtered
- Non-action root object still checked by authorizer
All 280+ core tests, 124 JSON tests, 76 REST tests pass with 0 regressions.
* WW-5624: v3 — fix indexed-path depth parity with ParametersInterceptor
Four gaps identified by lukaszlenart's April 10 review are now fully addressed:
1. JSON filterUnauthorizedList: pass prefix+"[0]" instead of bare prefix so
that list element properties gain one extra '[' in their path — e.g.
"publicPojoListDepthOne[0].key" (depth=2) is now correctly rejected when
@StrutsParameter(depth=1), matching ParametersInterceptor semantics.
Also recurse into nested List<List<Map>> via an else-if branch.
2. REST copyAuthorizedProperties: add authTarget parameter (always = root
action/model, passed unchanged through all recursion levels).
isAuthorized() now checks the full path against the root class, so
"address.city" is looked up on the action, not on the Address object.
3. REST Collection/Map/array deep authorization: replaced the as-is copy
with deepCopyAuthorizedCollection(), deepCopyAuthorizedMap(), and
deepCopyAuthorizedArray() helpers — each iterates elements with
path+"[0]" prefix, authorizing every complex element individually.
No-arg fallback skips the element rather than copying an unfiltered
object graph (security fix over plan's original as-is suggestion).
4. REST scrubUnauthorizedProperties: now fully recursive via
scrubUnauthorizedPropertiesRecursive() — visits nested beans,
collection elements, and map values with authTarget always pointing
to the root. Includes identity-based visited-set to guard against
circular reference cycles.
Tests: core 2920 + json 124 + rest 76 = 3120, 0 failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* WW-5624: v3.1 — fix collection type, identity set, isNestedBeanType coverage
Three correctness/security issues identified by independent review:
1. deepCopyAuthorizedCollection/deepCopyAuthorizedMap type preservation:
Previously always returned ArrayList/LinkedHashMap. If the action field
is typed Set<Pojo> or SortedMap<K,V>, writeMethod.invoke would throw
IllegalArgumentException. Now: SortedSet→TreeSet, Set→LinkedHashSet,
List→ArrayList; SortedMap→TreeMap, Map→LinkedHashMap.
2. scrubUnauthorizedPropertiesRecursive visited-set identity safety:
Replaced Set<Integer>+System.identityHashCode (not collision-safe) with
Collections.newSetFromMap(new IdentityHashMap<>()) which uses reference
equality (==). A hash collision could have caused a valid nested object
to be skipped, leaving unauthorized properties un-scrubbed.
3. isNestedBeanType now excludes all standard-library leaf packages:
java.util.* non-Collection/Map types (UUID, Currency, Locale, Date),
java.time.* (all temporal types, not just Temporal subinterface),
java.net.*, java.io.*, java.nio.*. Previously UUID etc. would return
true, causing the code to recurse into their internal fields and silently
drop the value when no @StrutsParameter annotation matched.
Tests: json 124 + rest 76 = 200, 0 failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* WW-5624: v4 — close bulk-copy fallback, reject body when no no-arg ctor
Two remaining gaps addressed per lukaszlenart's April 11 review:
1. copyAuthorizedProperties bulk-copy fallback removed:
When a nested target bean is null and createFreshInstance fails (no
no-arg constructor), the previous code fell back to
writeMethod.invoke(target, sourceValue) — copying the whole nested
object graph without per-path authorization. Now logs a warning and
skips the property entirely (same policy as deepCopyAuthorizedCollection
elements with no no-arg constructor).
2. Top-level no-arg constructor fallback changed from scrub to reject:
When requireAnnotations=true and the target class has no no-arg
constructor, body deserialization is now rejected entirely
(handler.toObject is never called). The previous best-effort scrub
path could not guarantee that all nested unauthorized properties were
nulled out. scrubUnauthorizedProperties and its recursive helper are
removed as dead code.
Tests: rest 76, 0 failures.
---------
Co-authored-by: tranquac <tranquac@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(core): HTML-encode form action in PostbackResult to prevent XSS
PostbackResult.doExecute() embeds finalLocation into a <form action="">
attribute via raw string concatenation without HTML encoding. A double
quote in the location breaks out of the attribute, enabling reflected
XSS. The response Content-Type is text/html (line 103).
This is an encoding inconsistency: form field names and values at lines
218-219 ARE properly URL-encoded via URLEncoder.encode(), but the form
action attribute was not encoded at all.
Add encodeHtml() to escape &, ", <, > in finalLocation before embedding
it in the HTML form tag, consistent with the existing encoding approach
for form field values in the same class.
* fix(core): WW-5623 use StringEscapeUtils and add regression tests
Address review feedback from @lukaszlenart:
- Replace custom encodeHtml() with StringEscapeUtils.escapeHtml4()
for consistency with the rest of Struts core (DefaultActionProxy,
Property, TextProviderHelper all use StringEscapeUtils)
- Add 3 focused unit tests in PostbackResultTest:
- testFormActionHtmlEscaping: XSS payload with attribute breakout
- testFormActionEscapesAllHtmlSpecialChars: covers ", &, <, >
- testFormActionCleanLocationUnchanged: regression for clean URLs
---------
Co-authored-by: tranquac <tranquac@users.noreply.github.com>
Detect Hibernate availability once at class-load time via Class.forName()
and short-circuit all Hibernate-related methods immediately when absent.
This eliminates repeated LinkageError/NoClassDefFoundError exceptions
that cause significant performance degradation in applications without
Hibernate on the classpath.
Fixes https://issues.apache.org/jira/browse/WW-5622
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Modern JDKs (7u45+) already protect against this attack with a built-in
64K entity expansion limit. These changes add defense-in-depth hardening
and remove unnecessary attack surface.
- Remove unused parseStringAsXML feature from StringAdapter to eliminate
a theoretical XML Entity Expansion vector
- Deprecate setParseStringAsXML() and getParseStringAsXML() for removal
- Enable SECURE_PROCESSING feature in DigesterDefinitionsReader
- Add unit test verifying JDK's entity expansion limit rejects
Billion Laughs payloads
- Add research document with vulnerability analysis
Co-authored-by: Claude <noreply@anthropic.com>
* WW-5537 Add InternalDestroyable and ContextAwareDestroyable interfaces
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* WW-5537 ContainerHolder: ThreadLocal with AtomicLong generation counter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* WW-5537 FinalizableReferenceQueue: volatile instance, join, classloader null
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* WW-5537 ScopeInterceptor.clearLocks: add synchronized block
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* WW-5537 CompoundRootAccessor, DefaultFileManager: implement InternalDestroyable
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* WW-5537 Add InternalDestroyable adapter classes for static cache cleanup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* WW-5537 Register InternalDestroyable beans in struts-beans.xml
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* WW-5537 JSON plugin: add JSONCacheDestroyable for BeanInfo cache cleanup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* WW-5537 Dispatcher.cleanup: refactor into focused methods with InternalDestroyable discovery
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* WW-5537 Rewrite DispatcherCleanupTest for InternalDestroyable discovery
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* WW-5537 Add log4j-web for proper Log4j2 lifecycle in Servlet container
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* WW-5537 Dispatcher.destroyObjectFactory: add early return on null, use pattern matching
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* WW-5537 Fix @since annotations: 7.1.0 -> 7.2.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* WW-5537 Add Container.destroy() to clear internal caches on undeploy
Container now exposes a destroy() method that clears factories, injectors,
constructors, and ThreadLocals. This releases Class<?> keys and JDK
DelegatingClassLoader instances that pin the webapp classloader.
DefaultConfiguration.destroy() calls container.destroy() and
reloadContainer() delegates to destroy() to avoid duplication.
Also fixes JSONCacheDestroyable referencing non-existent DefaultJSONWriter
(renamed to StrutsJSONWriter).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* WW-5537 Fix Container.destroy(): don't clear factories, don't call from reloadContainer
factories must remain intact because existing code holds direct
references to the Container after destroyConfiguration() and expects
it to still resolve dependencies (e.g. during configuration reload).
reloadContainer() reverted to clearing packageContexts/loadedFileNames
directly — calling destroy() there nulled the container reference and
cleared state needed during the bootstrap transition.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* WW-5537 Restore destroy() call in reloadContainer()
The test failures were caused by factories.clear() in
Container.destroy(), not by calling destroy() from reloadContainer().
Now that factories.clear() is removed, destroy() is safe to call
here — it clears packageContexts, loadedFileNames, and the container's
reflection caches in one place.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* WW-5537 Fix Sonar issues: thread-safe FinalizableReferenceQueue, empty method comments
- Replace volatile field with AtomicReference in FinalizableReferenceQueue
for proper thread safety using getAndSet()
- Add comments to empty destroy() implementations in test mocks
- Replace deprecated new URL() with URI.toURL() in DispatcherCleanupTest
- Add comments to empty listener methods in DispatcherCleanupTest
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add configurable limits to the JSON plugin to prevent denial-of-service
attacks via malicious payloads (deeply nested objects, huge arrays, long
strings).
Changes:
- Extract JSONReader interface from class, create StrutsJSONReader impl
with maxElements, maxDepth, maxStringLength, maxKeyLength enforcement
- Rename DefaultJSONWriter to StrutsJSONWriter (Struts* naming convention)
- Add JSONBeanSelectionProvider for bean aliasing via constants
- Update JSONUtil with @Inject for reader/writer, add instance
deserializeInput() with maxLength check, deprecate static deserialize()
- Wire limits into JSONInterceptor with @Inject from constants
- Register beans and defaults in struts-plugin.xml
Default limits: 10K elements, 64 depth, 2MB length, 256KB strings, 512 keys.
All configurable via struts.xml constants or per-action interceptor params.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Add production deployment warnings to showcase and rest-showcase READMEs
- Convert README.txt to README.md with proper Markdown formatting
- Restrict ViewSourceAction config parameter to XML files within webapp path
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace e.printStackTrace() with System.err.println() to properly
log errors to stderr without stack trace noise in CLI tools.
Issue: JRException handling in JasperReports compilation
Co-authored-by: Senrian <sen@senrian.com>
* WW-2963 fix(core): resolve default-action-ref via wildcard matching
When default-action-ref names an action that only exists as a wildcard
pattern (e.g., "movie-list" matching "movie-*"), the fallback now tries
wildcard matching after the exact map lookup fails. This mirrors the
exact→wildcard resolution already used for request action names.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* WW-2963 refactor(core): reduce cognitive complexity of findActionConfigInNamespace
Extract default-action-ref resolution into findDefaultActionConfig() and
replace the deeply nested if-pyramid with early returns, reducing the
nesting depth from 5 to 1 to satisfy Sonar's complexity threshold.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Made-with: Cursor
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Add serialization support for LocalDate, LocalDateTime, LocalTime,
ZonedDateTime, OffsetDateTime, and Instant in DefaultJSONWriter
- Add deserialization support for the same types in JSONPopulator
- Support @JSON(format=...) custom formats for all temporal types
- Fix Instant custom-format serialization requiring UTC zone
- Add Calendar serialization/deserialization via temporal bridge
- Add comprehensive tests for all temporal types including custom
formats, malformed input, and null handling
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Moved to user-wide ~/.claude/agents/ to make it available across all
projects. The agent is now project-agnostic and auto-detects build tools.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(i18n): ensure request_locale takes precedence over Accept-Language when supportedLocale is configured
When supportedLocale was configured on the I18nInterceptor, the Accept-Language
header match in AcceptLanguageLocaleHandler.find() returned early before
SessionLocaleHandler/CookieLocaleHandler ever checked their explicit locale
parameters (request_locale, request_cookie_locale). This made it impossible
to switch locale via request parameters when supportedLocale was set.
Changes:
- Reorder AcceptLanguageLocaleHandler.find() to check request_only_locale
before Accept-Language matching
- Reorder SessionLocaleHandler.find() to check request_locale before super
- Reorder CookieLocaleHandler.find() to check request_cookie_locale before super
- Add isLocaleSupported() helper to validate locales against supportedLocale
- Filter all locale sources (params, session, cookies) through supportedLocale
- Add 4 tests covering the bug scenario and supportedLocale filtering
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* test(i18n): cover missing supportedLocale locale-selection paths
Add regression tests for unsupported request_cookie_locale fallback, stored cookie revalidation, and request_only_locale precedence to lock in WW-5549 behavior across remaining branches.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(i18n): extract locale handlers with deprecated inner wrappers
Move locale handler implementations into a dedicated interceptor.i18n package with reusable abstract bases, keep thin deprecated inner wrappers in I18nInterceptor for one release-cycle compatibility, and document the LocaleHandler contract.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(i18n): validate request_only_locale against supportedLocale and fix Accept-Language fallback
RequestLocaleHandler.find() now checks isLocaleSupported() before
returning, preventing unsupported locales from slipping through via
the request_only_locale parameter. AcceptLanguageLocaleHandler.find()
now returns the first Accept-Language locale when supportedLocale is
empty, fixing ACCEPT_LANGUAGE storage mode with no filter configured.
Also includes refactoring: deprecated inner classes collapsed with
LocaleHandlerAdapter, shouldStore field encapsulated via disableStore(),
logger pattern standardized to private static final, and class-level
JavaDoc added to handler classes.
Made-with: Cursor
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Apply recommendations from CLAUDE.md benchmarking study (1,188 tests
across 3 models): remove redundant generic instructions, reframe
prohibitions as positive directives, trim inferable content, and
keep only project-specific knowledge that Claude cannot derive from
the codebase itself. Reduces file from 142 to 64 lines.
Key changes:
- Remove Common Pitfalls (negative framing, generic, duplicated)
- Remove Available Tools section (redundant with system prompt)
- Trim build commands to project-specific flags only
- Collapse Technology Stack into one-line overview
- Reframe security directives from "never do X" to "do Y instead"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
DefaultActionProxy.resolveMethod() unconditionally set methodSpecified=false
when the method was not passed explicitly, including when it was resolved from
ActionConfig (e.g., wildcard substitution like method="{1}"). This caused
HttpMethodInterceptor to skip method-level annotation checks for wildcard
actions, falling back to class-level annotations instead.
Move methodSpecified=false inside the inner branch that defaults to "execute",
so config-resolved methods (including wildcard-substituted ones) correctly
report isMethodSpecified()=true. Update Javadoc to reflect the corrected
semantics.
Fixes [WW-5535](https://issues.apache.org/jira/browse/WW-5535)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat(proxy): WW-5514 add StrutsProxyService for proxy detection and resolution
Introduces a configurable ProxyService interface and StrutsProxyService
implementation for detecting and resolving Spring AOP/Hibernate proxies.
Key changes:
- Add ProxyService interface with isProxy, ultimateTargetClass, and
resolveTargetMember methods
- Add StrutsProxyService implementation using configurable caches
- Add ProxyCacheFactory and StrutsProxyCacheFactory for cache management
- Integrate ProxyService into ChainingInterceptor, ParametersInterceptor,
and SecurityMemberAccess
- Add integration test with Spring AOP proxied action chaining
- Add configuration constants for proxy cache type and size
The StrutsProxyService correctly handles:
- Spring CGLIB proxies (class-based)
- Spring JDK dynamic proxies (interface-based)
- Hibernate entity proxies
- Member resolution for allowlist checking
Fixes WW-5514
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* test(proxy): WW-5514 add ProxyService integration tests for Spring proxies
Add integration tests to SpringProxyUtilTest that verify the new
ProxyService works correctly with real Spring AOP proxies, alongside
the existing deprecated ProxyUtil tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(proxy): WW-5514 address PR review feedback for proxy caches
Remove targetClassCache from StrutsProxyService to avoid memory leak
(object-keyed cache reintroduced from PR #1578). Change default proxy
cache type to wtlfu to align with all other caches. Switch deprecated
ProxyUtil static caches to BASIC to remove hard Caffeine dependency.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>