Summary (form: "Summary")
OBP-API caches computed values in Redis using Twitter Chill's KryoInjection, whose default Kryo pool
(ScalaKryoInstantiator.defaultPool) has registration not required and uses Objenesis
StdInstantiatorStrategy — i.e. it will instantiate arbitrary classes found in the serialized stream,
bypassing constructors. On every cache read, the raw bytes returned by Redis GET are passed straight to
KryoInjection.invert(...).
Consequently, any party who can write the OBP Redis keyspace can achieve remote code execution in the OBP-API
JVM by planting a malicious Kryo payload (a "deserialization gadget") under a cache key that OBP later reads back.
This is a second-order / conditional vulnerability: it is not triggerable by an HTTP request alone (a request
controls only which cache key is computed, not the serialized value bytes). The realistic precondition is
write access to the cache Redis — most commonly an exposed or unauthenticated Redis instance, a shared/multi-tenant
Redis, or any separate bug granting arbitrary Redis writes. The default Redis configuration (127.0.0.1:6379,
no password unless cache.redis.password is set) makes a misconfigured exposure plausible, and the consequence of
such a misconfiguration is upgraded from "cache tampering" to "RCE".
Severity (form: "Severity") and CVSS
Proposed: High, but explicitly gated on a precondition (cache Redis writable by attacker).
Suggested CVSS v3.1 vector (privileges-required reflects the Redis-write precondition):
CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H → ~6.8 (Medium–High)
Rationale for the non-RCE-default scoring (honest framing):
AC:H and PR:H capture that the attacker must already be able to write the backend Redis — not a property of an
unauthenticated HTTP request.
- If the deployment exposes an unauthenticated Redis on a routable interface, the effective barrier collapses and the
practical impact is full RCE; maintainers may prefer to score that deployment scenario higher.
Affected products / versions (form: "Affected products")
- Ecosystem: (no published package — this is an application; report against the repo)
- Package/repo:
OpenBankProject/OBP-API
- Affected versions: at least current
develop (version := "1.10.1", commit eeebff8, 2026-06-25) and prior releases
that contain obp-api/src/main/scala/code/api/cache/Redis.scala with the KryoInjection codec. The pattern has been
present for a long time and is also carried by sibling repos (see "Additional context").
- Patched versions: none
Dependency that supplies the unsafe default: com.twitter %% chill-bijection % 0.9.1 (and chill-akka % 0.9.1).
Vulnerability type / CWE
- CWE-502: Deserialization of Untrusted Data
- (contributing) CWE-1188 / insecure default configuration of the cache Redis (no auth by default)
Proof of concept / technical details (form: "Description" / details)
Sink — obp-api/src/main/scala/code/api/cache/Redis.scala
import com.twitter.chill.KryoInjection // L243
implicit def anyToByte[T](implicit m: Manifest[T]) = new Codec[T, Array[Byte]] {
def serialize(value: T): Array[Byte] = KryoInjection(value) // L247 cache WRITE
def deserialize(data: Array[Byte]): T = {
val tryDecode = KryoInjection.invert(data) // L254 SINK — unrestricted Kryo on Redis bytes
tryDecode match {
case Success(v) => v.asInstanceOf[T]
case Failure(e) => logger.error(e); "NONE".asInstanceOf[T]
}
}
}
implicit val scalaCache = ScalaCache(RedisCache(url, port)) // L238
KryoInjection (chill-bijection) delegates to ScalaKryoInstantiator.defaultPool. In chill,
ScalaKryoInstantiator configures:
k.setRegistrationRequired(false) // arbitrary classes allowed
k.setInstantiatorStrategy(new StdInstantiatorStrategy) // Objenesis: constructor bypassed
This is the well-known Kryo "deserialization gadget" condition (cf. CVE-2020-5413; the OWASP Deserialization Cheat
Sheet explicitly names Chill as a wrapper that leaves class registration not required by default).
Data flow
HTTP request -> provider method wrapped in Caching.memoize(Sync)WithProvider (code/api/cache/Caching.scala L13-L35)
-> Redis.memoize(Sync)WithRedis (Redis.scala L264-L270)
-> ScalaCache RedisCache GET <namespaced key> -> raw bytes
-> anyToByte.deserialize(bytes) -> KryoInjection.invert(bytes) [SINK]
Cached value types include metrics, FX rates, method-routing / endpoint-mapping / dynamic-entity lookups, auth/user
lookups, rate-limit state, etc. — all normally written by OBP itself via KryoInjection(value).
Trust boundary / why it is conditional (stated honestly)
The bytes fed to invert come from Redis GET. OBP trusts them because, in a correct deployment, only OBP writes
those keys. An HTTP attacker influences which cache key is computed (CacheKeyFromArguments.buildCacheKey) but not
the serialized value bytes. I did not find a first-order path where attacker-supplied raw bytes are stored to a
Redis key and later read back through anyToByte.deserialize. The exploit therefore requires the attacker to write the
Redis keyspace.
Redis defaults that make the precondition realistic — Redis.scala L28–L34
val url = APIUtil.getPropsValue("cache.redis.url", "127.0.0.1")
val port = APIUtil.getPropsAsIntValue("cache.redis.port", 6379)
val password = APIUtil.getPropsValue("cache.redis.password") match {
case Full(p) if p.trim.nonEmpty => p
case _ => null // no auth unless explicitly configured
}
Exploit outline (authorized lab only — do NOT run against production)
- Attacker obtains write access to the OBP cache Redis (e.g. exposed
6379 without cache.redis.password).
- Attacker writes, under a key OBP will read back via
memoize*WithRedis, a Kryo-serialized gadget payload
(an object graph whose instantiation triggers code execution / SSRF via classes on OBP's classpath).
- OBP performs a cache read for that key →
KryoInjection.invert(payload) → arbitrary class instantiation → RCE in
the OBP JVM.
Non-destructive validation: use a gadget that performs an out-of-band callback (DNS/HTTP) instead of real code
execution to prove deserialization fired.
Impact
Remote code execution in the OBP-API JVM (and thus access to anything that process can reach — DB credentials,
connector secrets, etc.), conditioned on the attacker being able to write the backend Redis. Where the cache Redis
is exposed/unauthenticated, the end-to-end impact is critical; where Redis is correctly isolated and authenticated, the
path is not reachable.
Remediation (recommended)
- Lock down the cache deserializer. Replace
KryoInjection.defaultPool with a Kryo instantiator that calls
setRegistrationRequired(true) and registers only the known cached value types (allowlist). This removes the
Redis-write → RCE upgrade even if Redis is compromised. Alternatively use a non-code-executing serializer
(typed JSON) for cache values.
- Harden Redis by default / documentation. Require authentication for the cache Redis; never bind it to a routable
interface; document that 127.0.0.1:6379 with no password is a development-only setting.
- Apply the same fix to the sibling copies of this code (see below).
Additional context — other repos carrying the same code (for the maintainers)
The identical KryoInjection cache codec appears in (confirmed via code search):
OpenBankProject/OBP-API — obp-api/src/main/scala/code/api/cache/Redis.scala
OpenBankProject/API-Explorer — src/main/scala/code/util/cache/Redis.scala (independent app; Redis defaults
127.0.0.1:6379, no password field at all)
OpenBankProject/OBP-API-II — obp-api/src/main/scala/code/api/cache/Redis.scala
- Third-party copies/derivatives:
InnoScripts2/OBP-API-develop, finscaleAI/obp-API, FinworxTech/OpenBankProject,
hkwany/OBP-API, eric-erki/OBP-API (mostly stale snapshots).
CVE request
I'd like this to receive a CVE. Please consider requesting one via GitHub's advisory workflow once you triage it
(GitHub acts as CNA for advisories on this repo). Root cause is the unsafe default in Chill's Kryo pool, but the correct
attribution here is the OBP-API product's use of KryoInjection.defaultPool for cache deserialization — this is
distinct from the underlying SnakeYAML/Kryo library CVEs. I'm happy to coordinate disclosure timing and provide a
non-destructive PoC in a private channel.
Summary (form: "Summary")
OBP-API caches computed values in Redis using Twitter Chill's
KryoInjection, whose default Kryo pool(
ScalaKryoInstantiator.defaultPool) has registration not required and uses ObjenesisStdInstantiatorStrategy— i.e. it will instantiate arbitrary classes found in the serialized stream,bypassing constructors. On every cache read, the raw bytes returned by Redis
GETare passed straight toKryoInjection.invert(...).Consequently, any party who can write the OBP Redis keyspace can achieve remote code execution in the OBP-API
JVM by planting a malicious Kryo payload (a "deserialization gadget") under a cache key that OBP later reads back.
This is a second-order / conditional vulnerability: it is not triggerable by an HTTP request alone (a request
controls only which cache key is computed, not the serialized value bytes). The realistic precondition is
write access to the cache Redis — most commonly an exposed or unauthenticated Redis instance, a shared/multi-tenant
Redis, or any separate bug granting arbitrary Redis writes. The default Redis configuration (
127.0.0.1:6379,no password unless
cache.redis.passwordis set) makes a misconfigured exposure plausible, and the consequence ofsuch a misconfiguration is upgraded from "cache tampering" to "RCE".
Severity (form: "Severity") and CVSS
Proposed: High, but explicitly gated on a precondition (cache Redis writable by attacker).
Suggested CVSS v3.1 vector (privileges-required reflects the Redis-write precondition):
CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H→ ~6.8 (Medium–High)Rationale for the non-RCE-default scoring (honest framing):
AC:HandPR:Hcapture that the attacker must already be able to write the backend Redis — not a property of anunauthenticated HTTP request.
practical impact is full RCE; maintainers may prefer to score that deployment scenario higher.
Affected products / versions (form: "Affected products")
OpenBankProject/OBP-APIdevelop(version := "1.10.1", commiteeebff8, 2026-06-25) and prior releasesthat contain
obp-api/src/main/scala/code/api/cache/Redis.scalawith theKryoInjectioncodec. The pattern has beenpresent for a long time and is also carried by sibling repos (see "Additional context").
Dependency that supplies the unsafe default:
com.twitter %% chill-bijection % 0.9.1(andchill-akka % 0.9.1).Vulnerability type / CWE
Proof of concept / technical details (form: "Description" / details)
Sink —
obp-api/src/main/scala/code/api/cache/Redis.scalaKryoInjection(chill-bijection) delegates toScalaKryoInstantiator.defaultPool. In chill,ScalaKryoInstantiatorconfigures:This is the well-known Kryo "deserialization gadget" condition (cf. CVE-2020-5413; the OWASP Deserialization Cheat
Sheet explicitly names Chill as a wrapper that leaves class registration not required by default).
Data flow
Cached value types include metrics, FX rates, method-routing / endpoint-mapping / dynamic-entity lookups, auth/user
lookups, rate-limit state, etc. — all normally written by OBP itself via
KryoInjection(value).Trust boundary / why it is conditional (stated honestly)
The bytes fed to
invertcome from RedisGET. OBP trusts them because, in a correct deployment, only OBP writesthose keys. An HTTP attacker influences which cache key is computed (
CacheKeyFromArguments.buildCacheKey) but notthe serialized value bytes. I did not find a first-order path where attacker-supplied raw bytes are stored to a
Redis key and later read back through
anyToByte.deserialize. The exploit therefore requires the attacker to write theRedis keyspace.
Redis defaults that make the precondition realistic —
Redis.scalaL28–L34Exploit outline (authorized lab only — do NOT run against production)
6379withoutcache.redis.password).memoize*WithRedis, a Kryo-serialized gadget payload(an object graph whose instantiation triggers code execution / SSRF via classes on OBP's classpath).
KryoInjection.invert(payload)→ arbitrary class instantiation → RCE inthe OBP JVM.
Non-destructive validation: use a gadget that performs an out-of-band callback (DNS/HTTP) instead of real code
execution to prove deserialization fired.
Impact
Remote code execution in the OBP-API JVM (and thus access to anything that process can reach — DB credentials,
connector secrets, etc.), conditioned on the attacker being able to write the backend Redis. Where the cache Redis
is exposed/unauthenticated, the end-to-end impact is critical; where Redis is correctly isolated and authenticated, the
path is not reachable.
Remediation (recommended)
KryoInjection.defaultPoolwith a Kryo instantiator that callssetRegistrationRequired(true)and registers only the known cached value types (allowlist). This removes theRedis-write → RCE upgrade even if Redis is compromised. Alternatively use a non-code-executing serializer
(typed JSON) for cache values.
interface; document that
127.0.0.1:6379with no password is a development-only setting.Additional context — other repos carrying the same code (for the maintainers)
The identical
KryoInjectioncache codec appears in (confirmed via code search):OpenBankProject/OBP-API—obp-api/src/main/scala/code/api/cache/Redis.scalaOpenBankProject/API-Explorer—src/main/scala/code/util/cache/Redis.scala(independent app; Redis defaults127.0.0.1:6379, no password field at all)OpenBankProject/OBP-API-II—obp-api/src/main/scala/code/api/cache/Redis.scalaInnoScripts2/OBP-API-develop,finscaleAI/obp-API,FinworxTech/OpenBankProject,hkwany/OBP-API,eric-erki/OBP-API(mostly stale snapshots).CVE request
I'd like this to receive a CVE. Please consider requesting one via GitHub's advisory workflow once you triage it
(GitHub acts as CNA for advisories on this repo). Root cause is the unsafe default in Chill's Kryo pool, but the correct
attribution here is the OBP-API product's use of
KryoInjection.defaultPoolfor cache deserialization — this isdistinct from the underlying SnakeYAML/Kryo library CVEs. I'm happy to coordinate disclosure timing and provide a
non-destructive PoC in a private channel.