[SECURITY] Unsafe Deserialization of Redis Cache Data
| Field | Details |
|---|---|
| Product | macrozheng/mall |
| Version reviewed | Commit dcaa93b (2026-09-15), v1.0.3-67-gdcaa93b |
| Affected component | Redis value serializer in mall-common/src/main/java/com/macro/mall/common/config/BaseRedisConfig.java:41 |
| Severity | High — provisional, where an attacker can reach Redis and write cache values without authentication |
| Weakness | CWE-502: Deserialization of Untrusted Data |
| Researcher | Juttikkhun Jirathanan, McAiden Research Lab |
| Analysis date | 2026-09-28 |
1. Description
The application trusts Java class names stored inside Redis cache values. If an attacker can change a cache value, they can ask the application to create an unexpected Java object the next time that value is read.
This happens because the Redis serializer enables Jackson default typing with LaissezFaireSubTypeValidator, which does not restrict allowed subtypes:
@Bean
public RedisSerializer<Object> redisSerializer() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL);
return new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
}The important settings are:
LaissezFaireSubTypeValidatordoes not provide an application-specific allowlist of permitted classes. Jackson's separate internal restrictions may still block certain classes.DefaultTyping.NON_FINALenables class-name metadata for non-final types.Object.classallows the serializer to return broadly typed objects instead of requiring a specific cache model.Visibility.ANYallows access to private fields, increasing the object state that deserialization can populate.
A named class must be available on the application's classpath and deserializable by Jackson for this to work. This does not allow an attacker to upload a new Java class through a cache value.
Attack prerequisites and affected paths
The attacker needs permission to write the relevant Redis cache keys. The application must then read a modified key. This is not a demonstrated unauthenticated HTTP attack against the application.
The reviewed deployment file exposes Redis on host port 6379 and does not configure a password:
# document/docker/docker-compose-env.yml
redis:
image: redis:7
command: redis-server --appendonly yes
ports:
- 6379:6379The reviewed application profiles also leave the Redis password empty. This creates a potential unauthenticated write path wherever network access and Redis's effective runtime configuration permit connections. Actual reachability was not tested against a live deployment; firewalls and other deployment controls can change this precondition.
The application caches administrator accounts, member accounts, and administrator resource lists. Relevant readers include:
// UmsAdminCacheServiceImpl:95
return (UmsAdmin) redisService.get(key);
// UmsMemberCacheServiceImpl:45
return (UmsMember) redisService.get(key);These casts occur after deserialization. A cast failure does not prevent an unexpected object's constructor from running first.
Impact and evidence limits
Confirmed in the original local test: the serializer instantiated a test class selected by the supplied JSON, and that class's constructor ran.
Potential impact: malicious cache entries could cause errors or service disruption when read. Redis write access also enables cache poisoning, including changes to account or permission data; that risk exists independently of unsafe type handling. The original test did not establish an authentication bypass or measure availability impact.
Remote code execution was not demonstrated. It would require a suitable class or chain of classes already available to the application. The original analysis reported that the evaluated known gadget candidates were blocked or absent from the resolved dependencies. This does not prove that every possible chain is unusable.
The original advisory proposed CVSS 3.1 scores of 8.6 (AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:H) for unauthenticated Redis access and 6.0 (AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:H) for restricted access. Treat these as provisional: the high availability impact was not demonstrated, and network isolation or Redis authentication alone does not establish the correct privileges metric. Confirm deployment access and impact before finalizing a score.
2. Proof of Concept (PoC)
Purpose and prerequisites
This local test uses the application's serializer configuration and resolved dependencies. It shows that supplied JSON can select a class and cause its constructor to execute. It does not need a running Redis server, make network connections, or demonstrate command execution.
Prerequisites: a checkout of the reviewed project, Maven, and a compatible JDK. Run the commands from the repository root.
Step 1 — Create the test file
Save the following as MallRedisPoc.java:
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.nio.charset.StandardCharsets;
public class MallRedisPoc {
static RedisSerializer<Object> mallSerializer() {
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL);
return new Jackson2JsonRedisSerializer<>(mapper, Object.class);
}
public static class Canary {
public static volatile boolean constructed = false;
public String anything;
public Canary() {
constructed = true;
}
}
public static void main(String[] args) {
String payload = "[\"" + Canary.class.getName()
+ "\",{\"anything\":\"x\"}]";
Object result = mallSerializer().deserialize(
payload.getBytes(StandardCharsets.UTF_8));
System.out.println("instantiated: " + result.getClass().getName());
System.out.println("constructor ran: " + Canary.constructed);
}
}The payload is equivalent to this Redis value:
["MallRedisPoc$Canary", {"anything": "x"}]Canary is deliberately added to the test classpath. It is not a class included in the deployed application.
Step 2 — Compile and run
mvn -q dependency:build-classpath -Dmdep.outputFile=/tmp/mall-poc-cp.txt \
-Dmdep.includeScope=runtime -pl mall-common
javac -cp "$(cat /tmp/mall-poc-cp.txt)" -d . MallRedisPoc.java
java -cp ".:$(cat /tmp/mall-poc-cp.txt)" MallRedisPocStep 3 — Check the result
The original analysis recorded this output with jackson-databind 2.21.2 and spring-data-redis 3.5.11:
instantiated: MallRedisPoc$Canary
constructor ran: trueThis confirms that the serializer accepted the class name from the JSON and invoked its constructor before any application-level cast could reject the result.
3. Recommendation
Restrict the types that can be deserialized
Prefer serializers configured for explicit cache types, such as UmsAdmin, UmsMember, and a typed list of UmsResource. Avoid accepting arbitrary Java class names from cache data. Separate serializers or cache configurations may be needed for different value types.
If polymorphic typing is required, replace LaissezFaireSubTypeValidator with a strict allowlist of the exact classes needed by the cache. Include only required model and container implementations after reviewing them. Avoid allowing entire packages or every subclass of broad types such as Collection or Map.
Changing to GenericJackson2JsonRedisSerializer alone is not a security fix. The important control is restricting which types the mapper accepts.
Remove the global setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY) setting where possible. Use normal accessors or narrowly scoped annotations for models that require special handling.
Restrict access to Redis
- Keep Redis on a private network accessible only to the application and authorized administrators. Remove the published host port when it is unnecessary, or bind it to an appropriate restricted interface.
- Configure Redis authentication or ACLs and provide matching credentials to the application through the deployment's secret-management mechanism.
- Limit Redis users to the commands and key patterns they need.
These controls reduce the chance of malicious cache writes. Apply them alongside the serializer fix.
P.S. Please create security policy and allow researcher to report security issue secretly in "Security and quality" section.
Source: macrozheng/mall