Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions obp-api/src/main/resources/props/sample.props.template
Original file line number Diff line number Diff line change
Expand Up @@ -1557,6 +1557,21 @@ personal_data_collection_consent_country_waiver_list = Austria, Belgium, Bulgari
# user_invitation_link_base_URL=strongly recommended to use top level domain name so that all nodes in the cluster share same URL


# --- Dynamic code execution kill-switch (security: RCE surface) ---
# Master switch for USER-GENERATED code that OBP compiles and executes at runtime:
# - Dynamic Resource Docs (/management/dynamic-resource-docs) -> compiles Scala
# - Connector Methods (/management/connector-methods) -> compiles Scala/JS/Java
# - Dynamic Message Docs (/management/dynamic-message-docs) -> compiles Scala/JS/Java
# - ABAC Rules (/management/abac-rules) -> compiles Scala
# When false, these endpoints return OBP-50020 and NOTHING is compiled or executed.
# This does NOT affect Dynamic Entities or Swagger Dynamic Endpoints (they store data /
# proxy HTTP and do not execute code).
# 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.)
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
dynamic_code_sandbox_enable=false
# Here is the default permissions if you set the dynamic_code_sandbox_enable = true. If you need more permission need to add it manually here.
Expand Down
28 changes: 24 additions & 4 deletions obp-api/src/main/scala/code/api/util/DynamicUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +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.
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
}

val toolBox: ToolBox[universe.type] = runtimeMirror(getClass.getClassLoader).mkToolBox()
private val memoClassPool = new Memo[ClassLoader, ClassPool]

Expand All @@ -54,6 +62,14 @@ object DynamicUtil extends MdcLoggable{
* @return compiled Full[function|object|class] or Failure
*/
def compileScalaCode[T](code: String): Box[T] = {
if (!dynamicCodeExecutionEnabled)
return Failure(ErrorMessages.DynamicCodeExecutionDisabled)
compileScalaCodeUnchecked[T](code)
}

// Used ONLY by DynamicUtil.Validation's props-driven config parsing (operator config,
// not user-generated code) so the app can still boot with the kill-switch off.
private def compileScalaCodeUnchecked[T](code: String): Box[T] = {
logger.trace(s"code.api.util.DynamicUtil.compileScalaCode.size is ${dynamicCompileResult.size()}")
val compiledResult: Box[Any] = dynamicCompileResult.computeIfAbsent(code, _ => {
val tree = try {
Expand Down Expand Up @@ -344,7 +360,7 @@ object DynamicUtil extends MdcLoggable{

val dynamicCodeSandboxPermissions = APIUtil.getPropsValue("dynamic_code_sandbox_permissions", "[]").trim
val scalaCodePermissioins = "List[java.security.Permission]"+dynamicCodeSandboxPermissions.replaceFirst("\\[","(").dropRight(1)+")"
val permissions:Box[List[java.security.Permission]] = DynamicUtil.compileScalaCode(scalaCodePermissioins)
val permissions:Box[List[java.security.Permission]] = DynamicUtil.compileScalaCodeUnchecked(scalaCodePermissioins)

// all Permissions put at here
// Here is the Java Permission document, please extend these permissions carefully.
Expand All @@ -368,7 +384,7 @@ object DynamicUtil extends MdcLoggable{

val dependenciesString = APIUtil.getPropsValue("dynamic_code_compile_validate_dependencies", "[]").trim
val scalaCodeDependencies = s"${DynamicUtil.importStatements}"+dependenciesString.replaceFirst("\\[","Map(").dropRight(1) +").mapValues(v => StringUtils.split(v, ',').map(_.trim).toSet)"
val dependenciesBox: Box[Map[String, Set[String]]] = DynamicUtil.compileScalaCode(scalaCodeDependencies)
val dependenciesBox: Box[Map[String, Set[String]]] = DynamicUtil.compileScalaCodeUnchecked(scalaCodeDependencies)

/**
* Compilation OBP Dependencies Guard, only checked the OBP methods, not scala/Java libraies(are checked during the runtime.).
Expand Down Expand Up @@ -452,7 +468,9 @@ object DynamicUtil extends MdcLoggable{

private val memoDynamicFunction = new Memo[String, Box[DynamicFunction]]

def createJsFunction(methodBody:String, bindingVars: Map[String, AnyRef] = Map.empty): Box[DynamicFunction] = memoDynamicFunction.memoize("Javascript:" + methodBody) {
def createJsFunction(methodBody:String, bindingVars: Map[String, AnyRef] = Map.empty): Box[DynamicFunction] =
if (!dynamicCodeExecutionEnabled) Failure(ErrorMessages.DynamicCodeExecutionDisabled)
else memoDynamicFunction.memoize("Javascript:" + methodBody) {
Box tryo {
val jsCode = s"""async function processor(args, callContext) {
$methodBody
Expand Down Expand Up @@ -495,7 +513,9 @@ object DynamicUtil extends MdcLoggable{

private val javaEngine = (new ScriptEngineManager).getEngineByName("java")

def createJavaFunction(methodBody:String): Box[DynamicFunction] = memoDynamicFunction.memoize("java:" + methodBody) {
def createJavaFunction(methodBody:String): Box[DynamicFunction] =
if (!dynamicCodeExecutionEnabled) Failure(ErrorMessages.DynamicCodeExecutionDisabled)
else memoDynamicFunction.memoize("java:" + methodBody) {
import com.openbankproject.commons.ExecutionContext.Implicits.global
import com.openbankproject.commons.util.JsonAliases.compactRender

Expand Down
1 change: 1 addition & 0 deletions obp-api/src/main/scala/code/api/util/ErrorMessages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,7 @@ object ErrorMessages {
val NotAllowedEndpoint = "OBP-50017: The endpoint is not enabled at this OBP API instance."
val UnderConstructionError = "OBP-50018: Under Construction Error."
val DatabaseConnectionClosedError = "OBP-50019: Cannot connect to the OBP database."
val DynamicCodeExecutionDisabled = "OBP-50020: User-generated dynamic code execution is disabled on this API instance. Set allow_user_generated_scala_code=true to enable."


// Connector Data Exceptions (OBP-502XX)
Expand Down
7 changes: 7 additions & 0 deletions obp-api/src/main/scala/code/api/v4_0_0/Http4s400.scala
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import code.model.BankX
import code.api.JsonResponseException
import code.api.util.AuthenticationType
import code.api.util.CommonsEmailWrapper.{EmailContent, sendHtmlEmail}
import code.api.util.DynamicUtil
import code.api.util.DynamicUtil.Validation
import code.api.dynamic.endpoint.helper.CompiledObjects
import code.api.dynamic.endpoint.helper.practise.DynamicEndpointCodeGenerator
Expand Down Expand Up @@ -9163,6 +9164,7 @@ object Http4s400 {
val cc = req.callContext
val rawBody = cc.httpBody.getOrElse("")
for {
_ <- code.util.Helper.booleanToFuture(DynamicCodeExecutionDisabled, cc = Some(cc)) { DynamicUtil.dynamicCodeExecutionEnabled }
jsonConnectorMethod <- NewStyle.function.tryons(
s"$InvalidJsonFormat The Json body should be the ${classOf[JsonConnectorMethod].getSimpleName}",
400, Some(cc)) {
Expand Down Expand Up @@ -9192,6 +9194,7 @@ object Http4s400 {
EndpointHelpers.executeAndRespond(req) { cc =>
val rawBody = cc.httpBody.getOrElse("")
for {
_ <- code.util.Helper.booleanToFuture(DynamicCodeExecutionDisabled, cc = Some(cc)) { DynamicUtil.dynamicCodeExecutionEnabled }
connectorMethodBody <- NewStyle.function.tryons(
s"$InvalidJsonFormat The Json body should be the ${classOf[JsonConnectorMethod].getSimpleName}",
400, Some(cc)) {
Expand Down Expand Up @@ -9373,6 +9376,7 @@ object Http4s400 {

private def createDynamicResourceDocImpl(bankId: Option[String], rawBody: String, cc: CallContext): Future[JsonDynamicResourceDoc] = {
for {
_ <- code.util.Helper.booleanToFuture(DynamicCodeExecutionDisabled, cc = Some(cc)) { DynamicUtil.dynamicCodeExecutionEnabled }
body <- NewStyle.function.tryons(
s"$InvalidJsonFormat The Json body should be the ${classOf[JsonDynamicResourceDoc].getSimpleName}",
400, Some(cc)) {
Expand All @@ -9391,6 +9395,7 @@ object Http4s400 {

private def updateDynamicResourceDocImpl(bankId: Option[String], dynamicResourceDocId: String, rawBody: String, cc: CallContext): Future[JsonDynamicResourceDoc] = {
for {
_ <- code.util.Helper.booleanToFuture(DynamicCodeExecutionDisabled, cc = Some(cc)) { DynamicUtil.dynamicCodeExecutionEnabled }
body <- NewStyle.function.tryons(
s"$InvalidJsonFormat The Json body should be the ${classOf[JsonDynamicResourceDoc].getSimpleName}",
400, Some(cc)) {
Expand Down Expand Up @@ -9671,6 +9676,7 @@ object Http4s400 {

private def createDynamicMessageDocImpl(bankId: Option[String], rawBody: String, cc: CallContext): Future[JsonDynamicMessageDoc] = {
for {
_ <- code.util.Helper.booleanToFuture(DynamicCodeExecutionDisabled, cc = Some(cc)) { DynamicUtil.dynamicCodeExecutionEnabled }
body <- NewStyle.function.tryons(
s"$InvalidJsonFormat The Json body should be the ${classOf[JsonDynamicMessageDoc].getSimpleName}",
400, Some(cc)) {
Expand All @@ -9693,6 +9699,7 @@ object Http4s400 {

private def updateDynamicMessageDocImpl(bankId: Option[String], dynamicMessageDocId: String, rawBody: String, cc: CallContext): Future[JsonDynamicMessageDoc] = {
for {
_ <- code.util.Helper.booleanToFuture(DynamicCodeExecutionDisabled, cc = Some(cc)) { DynamicUtil.dynamicCodeExecutionEnabled }
body <- NewStyle.function.tryons(
s"$InvalidJsonFormat The Json body should be the ${classOf[JsonDynamicMessageDoc].getSimpleName}",
400, Some(cc)) {
Expand Down
4 changes: 4 additions & 0 deletions obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import code.api.v6_0_0.JSONFactory600.UpdateViewJsonV600
import code.model._
import code.model.dataAccess.AuthUser
import code.users.{Users, DoobieUserQueries}
import code.api.util.DynamicUtil
import code.util.Helper.SILENCE_IS_GOLDEN
import com.openbankproject.commons.dto.GetProductsParam
import code.model.ModeratedTransaction
Expand Down Expand Up @@ -4588,6 +4589,7 @@ object Http4s600 {
EndpointHelpers.executeAndRespond(req) { implicit cc =>
val rawBody = cc.httpBody.getOrElse("")
for {
_ <- code.util.Helper.booleanToFuture(DynamicCodeExecutionDisabled, cc = Some(cc)) { DynamicUtil.dynamicCodeExecutionEnabled }
validateJson <- NewStyle.function.tryons(InvalidJsonFormat, 400, Some(cc)) {
com.openbankproject.commons.util.JsonAliases.parse(rawBody).extract[ValidateAbacRuleJsonV600]
}
Expand Down Expand Up @@ -5488,6 +5490,7 @@ object Http4s600 {
val rawBody = cc.httpBody.getOrElse("")
val user = cc.user.openOrThrowException(AuthenticatedUserIsRequired)
for {
_ <- Helper.booleanToFuture(DynamicCodeExecutionDisabled, cc = Some(cc)) { DynamicUtil.dynamicCodeExecutionEnabled }
createJson <- NewStyle.function.tryons(InvalidJsonFormat, 400, Some(cc)) {
com.openbankproject.commons.util.JsonAliases.parse(rawBody).extract[CreateAbacRuleJsonV600]
}
Expand Down Expand Up @@ -5542,6 +5545,7 @@ object Http4s600 {
val rawBody = cc.httpBody.getOrElse("")
val user = cc.user.openOrThrowException(AuthenticatedUserIsRequired)
for {
_ <- Helper.booleanToFuture(DynamicCodeExecutionDisabled, cc = Some(cc)) { DynamicUtil.dynamicCodeExecutionEnabled }
updateJson <- NewStyle.function.tryons(InvalidJsonFormat, 400, Some(cc)) {
com.openbankproject.commons.util.JsonAliases.parse(rawBody).extract[UpdateAbacRuleJsonV600]
}
Expand Down
Loading
Loading