Root cause
Injection & unsafe execution introduced
implement V2 mock server with CLI support and karate.start() integration
How AI contributed: Direct introductionPublished Jun 18, 2026 in
karatelabs/karate
The Claude-generated V2 mock server bound incoming request data, request headers, and request parameters into the Karate JavaScript engine, where the mock def and response handling recursively evaluated any embedded expression it found in them. With the full Java bridge enabled, a request body containing a Java.type('java.lang.Runtime') call executed as Java, turning an ordinary mock endpoint into remote code execution. The fix disables request-expression evaluation and Java interop by default, keeps request-derived values inert, and leaves mocks an explicit opt-in.
MockServer's handler in karate-core/src/main/java/io/karatelabs/core/MockServer.java passed caller-supplied values into the shared script engine without marking them untrusted, and StepExecutor.processEmbeddedExpressions then evaluated the embedded expressions it found there through the Java bridge that KarateJsBase installs.
Root cause
implement V2 mock server with CLI support and karate.start() integration
Fix
fix(mock): treat request data as untrusted — disable Java interop and request-expression evaluation by default
Case facts
@@ -0,0 +1,224 @@+/*+ * The MIT License+ *+ * Copyright 2025 Karate Labs Inc.+ *+ * Permission is hereby granted, free of charge, to any person obtaining a copy+ * of this software and associated documentation files (the "Software"), to deal+ * in the Software without restriction, including without limitation the rights+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+ * copies of the Software, and to permit persons to whom the Software is+ * furnished to do so, subject to the following conditions:+ *+ * The above copyright notice and this permission notice shall be included in+ * all copies or substantial portions of the Software.+ *+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+ * THE SOFTWARE.+ */+package io.karatelabs.core;++import io.karatelabs.common.FileUtils;+import io.karatelabs.common.Resource;+import org.slf4j.Logger;+import org.slf4j.LoggerFactory;++import javax.net.ssl.*;+import java.io.ByteArrayInputStream;+import java.io.InputStream;+import java.security.*;+import java.security.cert.Certificate;+import java.security.cert.CertificateFactory;+import java.security.cert.X509Certificate;+import java.security.spec.PKCS8EncodedKeySpec;+import java.util.Base64;++/**+ * Factory for creating SSLContext instances from SslConfig.+ */+public class SslContextFactory {++ private static final Logger logger = LoggerFactory.getLogger(SslContextFactory.class);++ /**+ * Create an SSLContext for client use (connecting to HTTPS servers).+ */+ public static SSLContext createClientContext(SslConfig config) {+ try {+ if (config.isTrustAll()) {+ return createTrustAllContext(config.getAlgorithm());+ }++ TrustManager[] trustManagers = loadTrustManagers(config);+ KeyManager[] keyManagers = loadKeyManagers(config);++ SSLContext ctx = SSLContext.getInstance(config.getAlgorithm());+ ctx.init(keyManagers, trustManagers, new SecureRandom());+ return ctx;+ } catch (Exception e) {+ throw new RuntimeException("failed to create client SSL context: " + e.getMessage(), e);+ }+ }++ /**+ * Create an SSLContext for server use (accepting HTTPS connections).+ */+ public static SSLContext createServerContext(SslConfig config) {+ try {+ if (config.getCertPath() == null) {+ // Generate self-signed certificate+ return CertificateGenerator.generateSelfSigned();+ }+ return loadFromPem(config.getCertPath(), config.getKeyPath(), config.getAlgorithm());+ } catch (Exception e) {+ throw new RuntimeException("failed to create server SSL context: " + e.getMessage(), e);+ }+ }++ /**+ * Create a trust-all SSLContext (accepts any certificate).+ */+ private static SSLContext createTrustAllContext(String algorithm) throws Exception {+ TrustManager[] trustManagers = new TrustManager[]{+ new X509TrustManager() {+ @Override@@ -0,0 +1,220 @@+/*+ * The MIT License+ *+ * Copyright 2025 Karate Labs Inc.+ *+ * Permission is hereby granted, free of charge, to any person obtaining a copy+ * of this software and associated documentation files (the "Software"), to deal+ * in the Software without restriction, including without limitation the rights+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+ * copies of the Software, and to permit persons to whom the Software is+ * furnished to do so, subject to the following conditions:+ *+ * The above copyright notice and this permission notice shall be included in+ * all copies or substantial portions of the Software.+ *+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+ * THE SOFTWARE.+ */+package io.karatelabs.core;++import java.util.Map;++/**+ * SSL/TLS configuration for both client and server contexts.+ * <p>+ * Client configuration (for HTTP client):+ * <ul>+ * <li>trustAll - Trust all server certificates (for self-signed)</li>+ * <li>keyStore/keyStorePassword/keyStoreType - Client certificate for mTLS</li>+ * <li>trustStore/trustStorePassword/trustStoreType - Custom trust store</li>+ * </ul>+ * <p>+ * Server configuration (for mock server):+ * <ul>+ * <li>certPath - PEM certificate file path</li>+ * <li>keyPath - PEM private key file path</li>+ * <li>clientAuth - Require client certificate (mTLS)</li>+ * </ul>+ */+public class SslConfig {++ // Trust configuration (client)+ private boolean trustAll;+ private String trustStore;+ private String trustStorePassword;+ private String trustStoreType;++ // Client certificate (mTLS)+ private String keyStore;+ private String keyStorePassword;+ private String keyStoreType;++ // Algorithm+ private String algorithm = "TLS";++ // Server-side (for MockServer)+ private String certPath;+ private String keyPath;+ private boolean clientAuth;+ private String trustStorePath; // For server-side client cert validation++ /**+ * Create a trust-all configuration (for self-signed certificates).+ */+ public static SslConfig trustAll() {+ SslConfig config = new SslConfig();+ config.setTrustAll(true);+ return config;+ }++ /**+ * Create an SslConfig from a map (for karate.configure('ssl', {...})).+ */+ @SuppressWarnings("unchecked")+ public static SslConfig fromMap(Map<String, Object> map) {+ SslConfig config = new SslConfig();++ if (map.containsKey("trustAll")) {+ config.setTrustAll(Boolean.TRUE.equals(map.get("trustAll")));+ }++ if (map.containsKey("keyStore")) {+ config.setKeyStore((String) map.get("keyStore"));+ }@@ -0,0 +1,259 @@+/*+ * The MIT License+ *+ * Copyright 2025 Karate Labs Inc.+ *+ * Permission is hereby granted, free of charge, to any person obtaining a copy+ * of this software and associated documentation files (the "Software"), to deal+ * in the Software without restriction, including without limitation the rights+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+ * copies of the Software, and to permit persons to whom the Software is+ * furnished to do so, subject to the following conditions:+ *+ * The above copyright notice and this permission notice shall be included in+ * all copies or substantial portions of the Software.+ *+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+ * THE SOFTWARE.+ */+package io.karatelabs.core;++import io.karatelabs.gherkin.Feature;+import io.karatelabs.io.http.HttpServer;+import io.karatelabs.js.SimpleObject;+import org.slf4j.Logger;+import org.slf4j.LoggerFactory;++import java.util.ArrayList;+import java.util.List;+import java.util.Map;++/**+ * Public API for creating mock servers from feature files.+ * <p>+ * Usage:+ * <pre>+ * MockServer server = MockServer.feature("api.feature")+ * .port(8080)+ * .arg(Map.of("key", "value"))+ * .start();+ *+ * // ... use server ...+ *+ * server.stop();+ * </pre>+ */+public class MockServer implements SimpleObject {++ private static final Logger logger = LoggerFactory.getLogger(MockServer.class);++ private final HttpServer httpServer;+ private final MockHandler handler;+ private final int port;++ private MockServer(HttpServer httpServer, MockHandler handler, int port) {+ this.httpServer = httpServer;+ this.handler = handler;+ this.port = port;+ }++ /**+ * Create a builder for a mock server from a feature file path.+ */+ public static Builder feature(String path) {+ return new Builder().feature(path);+ }++ /**+ * Create a builder for a mock server from a parsed Feature.+ */+ public static Builder feature(Feature feature) {+ return new Builder().feature(feature);+ }++ /**+ * Create a builder for a mock server from a Resource.+ */+ public static Builder feature(io.karatelabs.common.Resource resource) {+ return new Builder().feature(Feature.read(resource));+ }++ /**+ * Create a builder for a mock server from a feature string.+ * Useful for inline feature definitions in tests.+ */@@ -2904,6 +2904,21 @@ private void executeConfigure(Step step) { expr = step.getDocString(); } Object value;+ if (key.equals("javaBridgeEnabled")) {+ // Toggle Java interop (Java.type) for this engine. Mocks disable it by default+ // so untrusted request data evaluated as embedded expressions cannot reach Java+ // class loading; a trusted feature opts back in with `configure javaBridgeEnabled = true`.+ Object enabled = evalKarateExpression(expr);+ runtime.getKarate().setJavaBridgeEnabled(Boolean.parseBoolean(String.valueOf(enabled)));+ return;+ }+ if (key.equals("requestExpressionsEnabled")) {+ // Opt a mock back in to evaluating embedded expressions in request-derived data+ // (off by default). Re-opens the injection surface - only for trusted mocks.+ Object enabled = evalKarateExpression(expr);+ runtime.setRequestExpressionsEnabled(Boolean.parseBoolean(String.valueOf(enabled)));+ return;+ } if (key.equals("headers") || key.equals("cookies")) { // v1 parity: `configure headers = obj` (and cookies) must hold a live // reference to the source object so mutations made after the configure@@ -3174,6 +3189,13 @@ private Object processEmbeddedExpressions(Object value) { * </ul> */ private Object processEmbeddedExpressions(Object value, boolean lenient) {+ // Untrusted HTTP request data (Mock Server) must never be evaluated as embedded+ // expressions - returning it verbatim leaves any `#(...)` it carries as inert data.+ // Short-circuiting at the first request-derived container protects every nested value.+ // No-op for ordinary execution, where nothing is ever marked request-derived.+ if (runtime.isRequestDerived(value)) {+ return value;+ } if (value instanceof Node) { processXmlEmbeddedExpressions((Node) value); return value;@@ -98,6 +98,16 @@ public class ScenarioRuntime implements Callable<ScenarioResult>, KarateJsContex // Cookie jar for auto-sending responseCookies on subsequent requests (V1 compatibility) private final Map<String, Map<String, Object>> cookieJar = new LinkedHashMap<>(); + // Identity set of values that originated from an untrusted HTTP request (Mock Server only).+ // Such values must not have their `#(...)` strings evaluated as embedded expressions — that+ // would let a client inject Karate/Java code. Only MockHandler populates this; it stays empty+ // (and the guard below is free) for ordinary execution. See markRequestDerived / isRequestDerived.+ private final java.util.Set<Object> requestDerived =+ java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>());+ // When true a mock evaluates request-derived embedded expressions (the pre-fix behavior) -+ // opt-in only, since it re-opens the injection surface. Defaults to off (request data is data).+ private boolean requestExpressionsEnabled;+ // Debug support - step navigation private List<Step> steps; private int stepIndex;@@ -1290,6 +1300,51 @@ public KarateJs getKarate() { return karate; } + /**+ * Whether a mock should evaluate embedded expressions found in request-derived data.+ * Off by default (request data is treated as inert data); a mock can opt in via the+ * {@code MockServer.Builder} flag or {@code configure requestExpressionsEnabled = true}.+ */+ public boolean isRequestExpressionsEnabled() {+ return requestExpressionsEnabled;+ }++ public void setRequestExpressionsEnabled(boolean requestExpressionsEnabled) {+ this.requestExpressionsEnabled = requestExpressionsEnabled;+ }++ /** Forget all request-derived values; called by the Mock Server at the start of each request. */+ public void clearRequestDerived() {+ if (!requestDerived.isEmpty()) {+ requestDerived.clear();+ }+ }++ /**+ * Deep-mark a value (and every nested Map/List) as having originated from an untrusted HTTP+ * request, so {@code processEmbeddedExpressions} will leave its {@code #(...)} strings inert.+ */+ public void markRequestDerived(Object value) {+ if (value instanceof Map<?, ?> map) {+ if (requestDerived.add(value)) { // add returns false if already present - stops cycles+ for (Object v : map.values()) {+ markRequestDerived(v);+ }+ }+ } else if (value instanceof List<?> list) {+ if (requestDerived.add(value)) {+ for (Object v : list) {+ markRequestDerived(v);+ }+ }+ }+ }++ /** True if the value was marked by {@link #markRequestDerived}. Free when nothing is marked. */+ public boolean isRequestDerived(Object value) {+ return !requestDerived.isEmpty() && requestDerived.contains(value);+ }+ /** * Get the cookie jar for auto-sending cookies on subsequent requests. * Returns a map of cookie name to cookie details (value, path, domain, etc.).@@ -185,6 +185,8 @@ public static class Builder { private String keyPath; private SslContext customSslContext; private boolean watch;+ private boolean javaBridgeEnabled;+ private boolean requestExpressionsEnabled; private Builder() { }@@ -283,6 +285,31 @@ public Builder watch(boolean watch) { return this; } + /**+ * Enable Java interop ({@code Java.type(...)}) for mock features. OFF by default: a+ * mock that assigns request-derived data (request/requestHeaders/requestParams) would+ * otherwise evaluate attacker-controlled embedded expressions with full Java access,+ * which is a remote code execution risk. Only enable for mocks that are not exposed to+ * untrusted clients and genuinely need Java interop. Individual features can also opt in+ * with {@code configure javaBridgeEnabled = true}.+ */+ public Builder javaBridgeEnabled(boolean javaBridgeEnabled) {+ this.javaBridgeEnabled = javaBridgeEnabled;+ return this;+ }++ /**+ * Evaluate embedded {@code #(...)} expressions found in request-derived data+ * (request/requestHeaders/requestParams). OFF by default: attacker-controlled request+ * data is treated as inert data, not Karate code. Only enable for trusted mocks that+ * intentionally template off request content. Individual features can also opt in with+ * {@code configure requestExpressionsEnabled = true}.+ */+ public Builder requestExpressionsEnabled(boolean requestExpressionsEnabled) {+ this.requestExpressionsEnabled = requestExpressionsEnabled;+ return this;+ }+ /** * Start the mock server. */@@ -296,12 +323,12 @@ public MockServer start() { MockHandler handler; if (watch) {- ReloadingHandler reloadingHandler = new ReloadingHandler(features, args, pathPrefix);+ ReloadingHandler reloadingHandler = new ReloadingHandler(features, args, pathPrefix, javaBridgeEnabled, requestExpressionsEnabled); handler = reloadingHandler.getHandler(); requestHandler = reloadingHandler; logger.info("watch mode enabled - features will be reloaded when modified"); } else {- handler = new MockHandler(features, args, pathPrefix);+ handler = new MockHandler(features, args, pathPrefix, javaBridgeEnabled, requestExpressionsEnabled); requestHandler = handler; } @@ -334,12 +361,16 @@ private static class ReloadingHandler implements Function<HttpRequest, HttpRespo private final Map<String, Object> args; private final String pathPrefix;+ private final boolean javaBridgeEnabled;+ private final boolean requestExpressionsEnabled; private final Map<Resource, Long> watchedFiles = new LinkedHashMap<>(); private MockHandler handler; - ReloadingHandler(List<Feature> features, Map<String, Object> args, String pathPrefix) {+ ReloadingHandler(List<Feature> features, Map<String, Object> args, String pathPrefix, boolean javaBridgeEnabled, boolean requestExpressionsEnabled) { this.args = args; this.pathPrefix = pathPrefix;+ this.javaBridgeEnabled = javaBridgeEnabled;+ this.requestExpressionsEnabled = requestExpressionsEnabled; // Track file modification times for each feature for (Feature feature : features) {@@ -358,7 +389,7 @@ private static class ReloadingHandler implements Function<HttpRequest, HttpRespo } // Initialize handler- handler = new MockHandler(features, args, pathPrefix);+ handler = new MockHandler(features, args, pathPrefix, javaBridgeEnabled, requestExpressionsEnabled); } MockHandler getHandler() {@@ -400,7 +431,7 @@ public HttpResponse apply(HttpRequest request) { watchedFiles.putAll(newWatchedFiles); // Create new handler with reloaded features- handler = new MockHandler(reloadedFeatures, args, pathPrefix);+ handler = new MockHandler(reloadedFeatures, args, pathPrefix, javaBridgeEnabled, requestExpressionsEnabled); logger.info("reloaded {} feature(s)", reloadedFeatures.size());AI-assisted change ef4690910048a43bcc75182e7a36b31432d91fc4b7b3e285231a27f9db179ddb · Fix 67bddd89654b5dc861326c36cb70533f877efaeb638d7f5152cb63ef92c66e66