Browse Source

Added regression tests for /api/ruleChain/testScript

Two test layers covering the controller surface that the JVN PoC uses:

Java unit (Spring MockMvc):
  RuleChainControllerTest#testScriptForbiddenForCustomer asserts a
  customer JWT against POST /api/ruleChain/testScript returns 403,
  locking in the existing @PreAuthorize('TENANT_ADMIN') guard.

Black-box (live docker-compose):
  JsExecutorSandboxIsolationTest#testRuleChainScriptCannotReachHostProcess
  posts the JVN exploit payload as a tenant admin and asserts the
  response carries error='process is not defined'. End-to-end through
  tb-node -> Kafka -> tb-js-executor with use_sandbox=true.

Registered the new org.thingsboard.server.msa.security package in
the connectivity TestNG suite so the black-box runner picks it up.
Added a thin TestRestClient.testRuleChainScript() helper.
pull/15600/head
Sergey Matvienko 5 months ago
parent
commit
6d00e0432a
  1. 17
      application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java
  2. 10
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java
  3. 73
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/security/JsExecutorSandboxIsolationTest.java
  4. 1
      msa/black-box-tests/src/test/resources/connectivity.xml

17
application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java

@ -405,4 +405,21 @@ public class RuleChainControllerTest extends AbstractControllerTest {
return doPost("/api/ruleChain", ruleChain, RuleChain.class);
}
@Test
public void testScriptForbiddenForCustomer() throws Exception {
loginCustomerUser();
doPost("/api/ruleChain/testScript", (Object) """
{
"script": "return msg;",
"scriptType": "update",
"argNames": ["msg", "metadata", "msgType"],
"msg": "{}",
"metadata": {},
"msgType": "POST_TELEMETRY_REQUEST"
}
""")
.andExpect(status().isForbidden());
}
}

10
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java

@ -328,6 +328,16 @@ public class TestRestClient {
.statusCode(HTTP_OK);
}
public JsonNode testRuleChainScript(Object body) {
return given().spec(requestSpec)
.body(body)
.post("/api/ruleChain/testScript")
.then()
.statusCode(HTTP_OK)
.extract()
.as(JsonNode.class);
}
private String getUrlParams(PageLink pageLink) {
String urlParams = "pageSize={pageSize}&page={page}";
if (!isEmpty(pageLink.getTextSearch())) {

73
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/security/JsExecutorSandboxIsolationTest.java

@ -0,0 +1,73 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.msa.security;
import com.fasterxml.jackson.databind.JsonNode;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import org.thingsboard.server.msa.AbstractContainerTest;
import static org.assertj.core.api.Assertions.assertThat;
public class JsExecutorSandboxIsolationTest extends AbstractContainerTest {
@BeforeClass
public void beforeClass() {
testRestClient.login("tenant@thingsboard.org", "tenant");
}
@AfterClass
public void afterClass() {
testRestClient.resetToken();
}
/**
* Black-box regression for JVN#16937365: a tenant admin must not be able
* to escape the tb-js-executor sandbox via the host-realm prototype chain
* exposed through the script's `args` argument. Runs against the live
* docker-compose deployment, which uses script.use_sandbox=true and
* JS_EVALUATOR=remote (Kafka -> tb-js-executor).
*/
@Test
public void testRuleChainScriptCannotReachHostProcess() {
JsonNode response = testRestClient.testRuleChainScript("""
{
"script": "var F = args.constructor.constructor; var p = F('return process')(); return { reachedHost: !!(p && p.mainModule) };",
"scriptType": "update",
"argNames": ["msg", "metadata", "msgType"],
"msg": "{}",
"metadata": {},
"msgType": "POST_TELEMETRY_REQUEST"
}
""");
// The sandboxed run must reject the escape attempt: the host `process`
// global is not defined inside the sandbox realm, so executing the
// synthesized function `F("return process")` throws.
assertThat(response.has("error")).isTrue();
String error = response.get("error").asText();
assertThat(error)
.as("sandbox must block host-realm reach via args.constructor.constructor; full error: %s", error)
.contains("process is not defined");
// Defense in depth: even if the script somehow returned, output must
// not indicate that the host process was reached.
if (response.hasNonNull("output")) {
assertThat(response.get("output").asText()).doesNotContain("\"reachedHost\":true");
}
}
}

1
msa/black-box-tests/src/test/resources/connectivity.xml

@ -25,6 +25,7 @@
<package name="org.thingsboard.server.msa.edqs"/>
<package name="org.thingsboard.server.msa.cf"/>
<package name="org.thingsboard.server.msa.rule.node"/>
<package name="org.thingsboard.server.msa.security"/>
</packages>
</test>
</suite>
Loading…
Cancel
Save