* WW-5668 Add remove(key) to the OgnlCache abstraction
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* WW-5668 Bound the localized-text provider caches with configurable size
Converts bundlesMap, messageFormats and missingBundles to the existing
OgnlCache abstraction, configurable via struts.i18n.cacheType and
struts.i18n.cacheMaxSize (wtlfu / 10000 by default). The caches are kept
transient and rebuilt in readObject so the providers stay serializable,
and bundlesMap-related synchronization moves to a dedicated monitor since
the field is now reassignable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* WW-5668 Add opt-in request-locale resolution consistency to Dispatcher
Adds struts.locale.validateRequestLocale (default false) so request-derived
locales can be restricted to the JVM's available-locale set, matching what
I18nInterceptor already applies to its own locale sources.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* WW-5668 Keep the localized-text providers deserializable across a version upgrade
Pins serialVersionUID to the value implicitly computed for the pre-6.11.0 class
shape instead of 1L, so a session serialized by a 6.10.0 node still loads on a
6.11.0 one during a rolling upgrade rather than failing with InvalidClassException.
Such a stream carries no value for the new cache settings, and field initialisers
do not run during deserialization, so readObject restores their defaults before
rebuilding the caches - without that guard it failed with a NullPointerException.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* WW-5666 fix(json): apply the input length limit while reading
* WW-5666 fix(core): bound the CSP report body read and make the limit configurable
CspReportAction read the submitted report body with a single readLine() and had
no limit of its own. Read it up to a limit instead, defaulting to 8192
characters and configurable through struts.csp.report.maxSize. A body above the
limit is discarded with a warning rather than processed.
The limit is injected when the action is built, before the interceptor stack
runs, because withServletRequest is invoked by the servletConfig interceptor
ahead of staticParams and params. Values that are not usable as a buffer size
are ignored with a warning.
Port of #1740 to 6.x: 12 overlapping cache tests collapsed to 5 focused
ones, JUnit 3 -> JUnit 4, dropped the ~80-class literal, reflection kept
only in the two size-bound tests. Also reorders the caffeine imports in
ConfigParseUtil to match alphabetical ordering. Production logic unchanged.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* WW-5630 - Performance Issue SecurityMemberAccess
* Add size bound cache, 50, for Class lookup
* Add unit test
Code generated by Copilot
* WW-5630 - Add additional UT
* WW-5630 - Add UT for non-existent class
* WW-5630 - Review feedback changes
* Cache ClassLoader directly
* Use weakKeys and weakValues
* Comment on the ClassLookupException
* Additional Unit Tests
Assistance in coding using co-pilot
* WW-5630 - Additional review
* Limit outer, Classloader, to 25. Ensure memory bounding.
* Limit inner, Classes, to 50. Ensure memory bounding.
* Additional UTs
With co-pilot assitance
(cherry picked from commit 210dc86b88)
PostbackResult.doExecute() embeds finalLocation into a <form action="">
attribute via raw string concatenation. A double quote in the location
breaks out of the attribute, enabling reflected XSS. The response
Content-Type is text/html.
Form field names and values elsewhere in the same class are properly
URL-encoded via URLEncoder.encode(); the action attribute was not
encoded at all.
Wrap finalLocation with StringEscapeUtils.escapeHtml4() before embedding
it in the form tag, consistent with the encoding approach used in
DefaultActionProxy, Property, and TextProviderHelper.
Adds three regression tests in PostbackResultTest:
- testFormActionHtmlEscaping: XSS payload with attribute breakout
- testFormActionEscapesAllHtmlSpecialChars: covers ", &, <, >
- testFormActionCleanLocationUnchanged: regression for clean URLs
The WW-5535 change to DefaultActionProxy.resolveMethod() (which made
wildcard-resolved methods report isMethodSpecified()=true) interacted
with HttpMethodInterceptor's if/else-if so that the class-level
annotation branch became unreachable when the resolved method carried
no method-level annotation:
if (isMethodSpecified()) {
if (method has annotation) return doIntercept(method);
// unannotated method falls through silently
} else if (class has annotation) {
return doIntercept(class); // never reached when methodSpecified=true
}
Convert the else-if to a standalone if so the class-level check is
always evaluated as a fallback. Method-level annotations still take
precedence — they are checked first and return early.
Adds three tests:
- testWildcardResolvedUnannotatedMethodRespectsClassLevelAnnotation:
GET on a wildcard-resolved unannotated method is rejected when the
class is @AllowedHttpMethod(POST).
- testWildcardResolvedUnannotatedMethodAllowsPostWithClassLevelAnnotation:
POST on the same configuration succeeds.
- testWildcardResolvedExecuteRejectsGetThroughRealProxy: end-to-end
via a real DefaultActionProxy with <action name="Wild-*" method="{1}">,
resolving to ActionSupport.execute().
Detect Hibernate availability once at class-load time via Class.forName()
and short-circuit all Hibernate-related methods immediately when absent.
This eliminates repeated NoClassDefFoundError exceptions that cause
significant performance degradation in applications without Hibernate
on the classpath.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Backport of apache/struts#1642 from Struts 7 to Struts 6.
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.
- Enable FEATURE_SECURE_PROCESSING in DomHelper SAX parser
- Enable FEATURE_SECURE_PROCESSING in DigesterDefinitionsReader
- Remove unused parseStringAsXML feature from StringAdapter to eliminate
a theoretical XML Entity Expansion vector
- Deprecate setParseStringAsXML() and getParseStringAsXML() for removal
- Add Billion Laughs protection tests for DomHelper and DigesterDefinitionsReader
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* WW-5537 fix(core): resolve classloader/memory leaks during Tomcat hot deployment
Introduce InternalDestroyable interface with container-based discovery to
clean up static caches, daemon threads, and shared references that pin the
webapp classloader after undeploy. This prevents OutOfMemoryError (Metaspace)
on repeated hot deployments.
Changes:
- Add InternalDestroyable/ContextAwareDestroyable interfaces for cleanup hooks
- Clear OGNL, Component, ScopeInterceptor, DefaultFileManager static caches
- Stop FinalizableReferenceQueue daemon thread and null its classloader
- Clear FreeMarker template/introspection caches from ServletContext
- Replace ContainerHolder ThreadLocal with volatile to prevent thread-pool leaks
- Clear static dispatcherListeners list on Dispatcher cleanup
- Add JSONCacheDestroyable for json plugin cache cleanup
- Register all destroyables via struts-beans.xml / struts-plugin.xml
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* WW-5537 chore(showcase): add log4j-web for proper Log4j2 lifecycle in Servlet container
Without log4j-web, Log4j2 SoftReferences delay classloader GC after undeploy.
The log4j-web module provides Log4jServletContextListener which ensures proper
Log4j2 shutdown during ServletContext destruction.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* WW-5537 fix(core): use ThreadLocal with generation counter in ContainerHolder
Replace the volatile shared reference with a ThreadLocal backed by a volatile
generation counter. Per-request clear() only affects the current thread (safe
for concurrent requests and tests). On undeploy, invalidateAll() advances the
generation counter so idle pool threads detect staleness on next access and
self-clear, preventing classloader leaks without breaking test isolation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a default-action-ref points to an action name that only exists as a
wildcard pattern (e.g. "movie-input" matching "movie-*"), the framework
now falls back to wildcard matching after the exact lookup fails.
Port of PR #1614 from Struts 7 to Struts 6.x.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When supportedLocale is configured on I18nInterceptor, request_locale
and request_cookie_locale parameters were ignored because
AcceptLanguageLocaleHandler.find() matched the Accept-Language header
before session/cookie handlers checked their explicit locale parameters.
Additionally, stored locales (session/cookie) were never validated
against supportedLocale.
Changes:
- Add isLocaleSupported() helper to validate locales against config
- RequestLocaleHandler.find() now validates against supportedLocale
- AcceptLanguageLocaleHandler.find() checks request_only_locale first,
then falls back to Accept-Language matching
- SessionLocaleHandler.find() checks request_locale before super.find()
- CookieLocaleHandler.find() checks request_cookie_locale before
super.find()
- SessionLocaleHandler.read() discards stale session locales
- CookieLocaleHandler.read() discards stale cookie locales
Port of PR #1594 (bug fix only, no refactoring)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
DefaultActionProxy.resolveMethod() incorrectly set methodSpecified=false
for config-resolved methods (including wildcard-substituted ones), causing
HttpMethodInterceptor to skip method-level @HttpPost/@HttpGet annotation
checks. Move methodSpecified=false inside the inner if block so it only
applies when truly defaulting to "execute".
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* Pull aspects into alignment with main/7.x+ AbstractMultiPartRequest.java
* Update JakartaMultiPartRequest and JakartaStreamMultiPartRequest to use isFile()
* Update cleanup text to mirror main/7.x+
* fix(ognl): make ProxyUtil cache configurable via struts constants
Makes the ProxyUtil cache type configurable through Struts constants,
allowing applications to use BASIC cache type (default) without
requiring Caffeine as a mandatory dependency.
New configuration properties:
- struts.proxy.cacheType: basic (default), lru, or wtlfu
- struts.proxy.cacheMaxSize: 10000 (default)
Fixes WW-5514
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(ognl): use LazyRef for proxy caches and reset on factory change
Extract lazy initialization into reusable LazyRef<T> utility with
double-checked locking and reset support. ProxyUtil.setProxyCacheFactory()
now resets both caches so they are recreated with the new factory,
fixing the bug where caches were never refreshed after factory changes.
Default proxy cache type changed from 'basic' to 'wtlfu' for consistency
with expression and beanInfo caches. Fix @since version to 6.9.0.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Implement two-phase processing for conversion properties to enable
Spring bean name resolution in struts-conversion.properties files.
The issue was a timing problem: type converters were processed during
bootstrap phase before SpringObjectFactory was available. Now:
- Early phase: process struts-default-conversion.properties (class names)
- Late phase: process user properties when SpringObjectFactory is ready
Changes:
- Add UserConversionPropertiesProvider interface for late initialization
- Add UserConversionPropertiesProcessor to trigger late phase processing
- Split StrutsConversionPropertiesProcessor.init() into early/late phases
- Register new beans in DefaultConfiguration and struts-beans.xml
- Add alias in StrutsBeanSelectionProvider for dependency injection
- Improve JavaDocs for BeanSelectionProvider classes
Closes WW-4291
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Evaluates contentCharSet expression before emptiness check to prevent
malformed content-type headers when expression evaluates to null.
- Parse contentCharSet expression first, then check if result is empty
- Use StringUtils.isNotEmpty() for proper null/empty validation
- Use setCharacterEncoding() instead of appending to content-type string
- Add test for null-evaluating charset expressions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
It should delegate back to ObjectFactory#buildBean instead of directly
calling Container#inject, otherwise overrides of buildBean in subclasses
of ObjectFactory e.g. SpringObjectFactory are skipped; meaning that
TypeConverters cannot make use of Spring dependency injection
Fixes: https://issues.apache.org/jira/projects/WW/issues/WW-5524