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}" \