From ac8d18cf590ca28d2428bd23b3acab3275baaef5 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Mon, 13 Jul 2026 16:13:35 +0200 Subject: [PATCH] fix(security): default the dynamic-code kill-switch to off in every run mode allow_user_generated_scala_code previously defaulted to true in test/dev run modes when unset, only defaulting to false in production. Per review feedback, remove the run-mode fallback entirely: the prop must now be set explicitly to true anywhere, including test and dev, or dynamic Scala/JS/Java compilation stays disabled. Set the prop explicitly in the CI "Setup props" steps and in test.default.props.template / run_tests_parallel.sh so the existing dynamic-code test suites keep exercising the ON path locally and in CI. Update DynamicCodeKillSwitchTest's predicate scenarios to reflect that the suite's baseline is now an explicit prop value, not an implicit test-mode default. --- .github/workflows/build_container.yml | 5 ++++ .github/workflows/build_pull_request.yml | 5 ++++ .../resources/props/sample.props.template | 3 ++- .../props/test.default.props.template | 6 +++++ .../scala/code/api/util/DynamicUtil.scala | 8 ++++--- .../v4_0_0/DynamicCodeKillSwitchTest.scala | 23 ++++++++++++++----- run_tests_parallel.sh | 6 +++++ 7 files changed, 46 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build_container.yml b/.github/workflows/build_container.yml index 27c60a6df2..f9f87b6762 100644 --- a/.github/workflows/build_container.yml +++ b/.github/workflows/build_container.yml @@ -301,6 +301,11 @@ jobs: # can do JSON extraction (reflection) and read OBP props (getenv); without it the sandbox # denies these and DynamicResourceDocTest's native-execution scenarios fail. echo 'dynamic_code_sandbox_permissions=[new java.net.NetPermission("specifyStreamHandler"), new java.lang.reflect.ReflectPermission("suppressAccessChecks"), new java.lang.RuntimePermission("getenv.*"), new java.util.PropertyPermission("cglib.useCache", "read"), new java.util.PropertyPermission("net.sf.cglib.test.stressHashCodes", "read"), new java.util.PropertyPermission("cglib.debugLocation", "read"), new java.lang.RuntimePermission("accessDeclaredMembers"), new java.lang.RuntimePermission("getClassLoader")]' >> obp-api/src/main/resources/props/test.default.props + # allow_user_generated_scala_code defaults to false everywhere (including test/dev) + # with no run-mode fallback; CI must opt in explicitly so DynamicUtilTest, + # ConnectorMethodTest, AbacRuleTests, DynamicResourceDocTest, DynamicMessageDocTest + # and DynamicCodeKillSwitchTest's ON scenarios can still compile/execute dynamic code. + echo allow_user_generated_scala_code=true >> obp-api/src/main/resources/props/test.default.props - name: Run tests — shard ${{ matrix.shard }} (${{ matrix.name }}) run: | diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index dbda7f5d3b..39716fe115 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -295,6 +295,11 @@ jobs: # can do JSON extraction (reflection) and read OBP props (getenv); without it the sandbox # denies these and DynamicResourceDocTest's native-execution scenarios fail. echo 'dynamic_code_sandbox_permissions=[new java.net.NetPermission("specifyStreamHandler"), new java.lang.reflect.ReflectPermission("suppressAccessChecks"), new java.lang.RuntimePermission("getenv.*"), new java.util.PropertyPermission("cglib.useCache", "read"), new java.util.PropertyPermission("net.sf.cglib.test.stressHashCodes", "read"), new java.util.PropertyPermission("cglib.debugLocation", "read"), new java.lang.RuntimePermission("accessDeclaredMembers"), new java.lang.RuntimePermission("getClassLoader")]' >> obp-api/src/main/resources/props/test.default.props + # allow_user_generated_scala_code defaults to false everywhere (including test/dev) + # with no run-mode fallback; CI must opt in explicitly so DynamicUtilTest, + # ConnectorMethodTest, AbacRuleTests, DynamicResourceDocTest, DynamicMessageDocTest + # and DynamicCodeKillSwitchTest's ON scenarios can still compile/execute dynamic code. + echo allow_user_generated_scala_code=true >> obp-api/src/main/resources/props/test.default.props - name: Run tests — shard ${{ matrix.shard }} (${{ matrix.name }}) run: | diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index d5616d5621..82b032707d 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -1571,7 +1571,8 @@ personal_data_collection_consent_country_waiver_list = Austria, Belgium, Bulgari # SECURITY: the runtime sandbox (SecurityManager) is a no-op on JDK 24+ (JEP 486) and the # GraalVM JS context is created with HostAccess.ALL, so enabled dynamic code runs UNSANDBOXED. # Leave false in production. Only enable on trusted sandbox/dev instances. -# (Always ON in test/dev run modes regardless of this value, unless this prop is set explicitly.) +# Defaults to false everywhere (including test/dev run modes) when this prop is absent — +# it must be set to true explicitly, there is no run-mode-based fallback. allow_user_generated_scala_code=false # enable dynamic code sandbox, default is false, this will make sandbox works for code running in Future, will make performance lower than disable diff --git a/obp-api/src/main/resources/props/test.default.props.template b/obp-api/src/main/resources/props/test.default.props.template index 240f3312f9..21f8856d41 100644 --- a/obp-api/src/main/resources/props/test.default.props.template +++ b/obp-api/src/main/resources/props/test.default.props.template @@ -147,6 +147,12 @@ allow_public_views =true # the 10-thread concurrency tests. Set to 20 to provide headroom. hikari.maximumPoolSize=20 +# allow_user_generated_scala_code (RCE surface kill-switch) defaults to false everywhere, +# including test/dev, with no run-mode fallback. Set explicitly here so DynamicUtilTest, +# ConnectorMethodTest, AbacRuleTests, DynamicResourceDocTest, DynamicMessageDocTest and +# DynamicCodeKillSwitchTest's ON scenarios can compile/execute dynamic code locally. +allow_user_generated_scala_code=true + # Permissions granted to runtime-compiled dynamic-endpoint code inside the security sandbox. # Mirrors default.props / production.default.props. Required so dynamic resource-doc bodies can do # JSON extraction (reflection) and read OBP props (getenv); without it the sandbox denies these and diff --git a/obp-api/src/main/scala/code/api/util/DynamicUtil.scala b/obp-api/src/main/scala/code/api/util/DynamicUtil.scala index fdf4fa7661..878e3bc08c 100644 --- a/obp-api/src/main/scala/code/api/util/DynamicUtil.scala +++ b/obp-api/src/main/scala/code/api/util/DynamicUtil.scala @@ -32,12 +32,14 @@ import scala.tools.reflect.{ToolBox, ToolBoxError} object DynamicUtil extends MdcLoggable{ - // Master kill-switch for user-generated dynamic code (RCE surface). Explicit prop always - // wins so tests can force OFF; absent the prop, ON in test/dev, OFF in production. + // Master kill-switch for user-generated dynamic code (RCE surface). Defaults to OFF + // everywhere — including test/dev — unless explicitly enabled via this prop. Tests that + // exercise dynamic-code compilation must set allow_user_generated_scala_code=true + // explicitly (see test.default.props / the CI "Setup props" step). def dynamicCodeExecutionEnabled: Boolean = APIUtil.getPropsValue("allow_user_generated_scala_code") match { case Full(v) => v.toBoolean - case _ => net.liftweb.util.Props.testMode || net.liftweb.util.Props.devMode + case _ => false } val toolBox: ToolBox[universe.type] = runtimeMirror(getClass.getClassLoader).mkToolBox() diff --git a/obp-api/src/test/scala/code/api/v4_0_0/DynamicCodeKillSwitchTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/DynamicCodeKillSwitchTest.scala index 205987e57f..2baa9aa41e 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/DynamicCodeKillSwitchTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/DynamicCodeKillSwitchTest.scala @@ -58,18 +58,26 @@ class DynamicCodeKillSwitchTest extends V400ServerSetup { setPropsValues("connector" -> "star") } + // The predicate defaults to false everywhere (including test/dev) with no run-mode + // fallback — it is false unless allow_user_generated_scala_code is explicitly set. This + // suite's baseline test.default.props sets it to true explicitly so the ON scenarios + // below can compile/execute dynamic code; there is no way, within this harness, to + // exercise the truly-absent case, since the base props file always supplies a value once + // set. The "absent -> false" branch is what protects deployers who never set the prop at + // all — it's covered by direct inspection of DynamicUtil.dynamicCodeExecutionEnabled's + // match expression, not by an integration test. feature("DynamicUtil.dynamicCodeExecutionEnabled predicate") { - scenario("Defaults to enabled in test mode when the prop is absent", VersionOfApi) { - Then("the predicate should be true (we are running under testMode)") + scenario("Explicit prop=true (this suite's baseline) enables compilation", VersionOfApi) { + Then("the predicate should be true given the explicit test-props value") DynamicUtil.dynamicCodeExecutionEnabled should be(true) - And("compileScalaCode should still compile and execute") + And("compileScalaCode should compile and execute") val result = DynamicUtil.compileScalaCode[Int]("41 + 1") result should be(Full(42)) } - scenario("Explicit prop=false overrides testMode and disables compilation", VersionOfApi) { + scenario("Explicit prop=false disables compilation regardless of run mode", VersionOfApi) { setPropsValues("allow_user_generated_scala_code" -> "false") Then("the predicate should be false") @@ -80,10 +88,13 @@ class DynamicCodeKillSwitchTest extends V400ServerSetup { result should be(Failure(DynamicCodeExecutionDisabled)) } - scenario("Explicit prop=true overrides a false default", VersionOfApi) { + scenario("A later explicit prop=true re-enables after being forced off", VersionOfApi) { + setPropsValues("allow_user_generated_scala_code" -> "false") + DynamicUtil.dynamicCodeExecutionEnabled should be(false) + setPropsValues("allow_user_generated_scala_code" -> "true") - Then("the predicate should be true") + Then("the predicate should be true again") DynamicUtil.dynamicCodeExecutionEnabled should be(true) } } diff --git a/run_tests_parallel.sh b/run_tests_parallel.sh index 4f8e687328..82c72d486c 100755 --- a/run_tests_parallel.sh +++ b/run_tests_parallel.sh @@ -278,6 +278,11 @@ run_shard() { # OBP_DYNAMIC_CODE_SANDBOX_PERMISSIONS mirrors CI's dynamic_code_sandbox_permissions # props line: without it the dynamic-code sandbox denies reflection/getenv and # DynamicResourceDocTest's native-execution scenarios fail locally (CI green, local red). + # OBP_ALLOW_USER_GENERATED_SCALA_CODE mirrors CI's allow_user_generated_scala_code=true: + # the kill-switch defaults to false everywhere (including test/dev) with no run-mode + # fallback, so DynamicUtilTest / ConnectorMethodTest / AbacRuleTests / + # DynamicResourceDocTest / DynamicMessageDocTest / DynamicCodeKillSwitchTest's ON + # scenarios need this set explicitly or they fail locally with OBP-50020. # -pl obp-commons,obp-api mirrors CI: obp-commons' own util suites run on whichever # shard's filter matches com.openbankproject.* (the shard-4 catch-all); on every other # shard the filter matches nothing in obp-commons -> 0 tests there. @@ -291,6 +296,7 @@ run_shard() { OBP_HTTP4S_TEST_PORT="${http4s_port}" \ OBP_MAIL_TEST_MODE="true" \ OBP_DYNAMIC_CODE_SANDBOX_PERMISSIONS='[new java.net.NetPermission("specifyStreamHandler"), new java.lang.reflect.ReflectPermission("suppressAccessChecks"), new java.lang.RuntimePermission("getenv.*"), new java.util.PropertyPermission("cglib.useCache", "read"), new java.util.PropertyPermission("net.sf.cglib.test.stressHashCodes", "read"), new java.util.PropertyPermission("cglib.debugLocation", "read"), new java.lang.RuntimePermission("accessDeclaredMembers"), new java.lang.RuntimePermission("getClassLoader")]' \ + OBP_ALLOW_USER_GENERATED_SCALA_CODE="true" \ OBP_API_INSTANCE_ID="shard_${n}" \ "$TIMEOUT_BIN" 1200 mvn scalatest:test -pl obp-commons,obp-api -DfailIfNoTests=false \ "-DwildcardSuites=${filter}" \