Skip to main content
SAP Pentest Playbook
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Groovy Script Step Attack Surface

Description

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.URL make outbound HTTPS calls directly, with no adapter involved. java.lang and java.net are 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 parallel KeystoreService decrypt 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.

Risk

Anyone who can deploy - or get someone/something else (eg. Code Injection) to deploy - a single iFlow with a Groovy Script step obtains:

  1. 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;
  2. arbitrary OS command execution on the shared CPI runtime node;
  3. an outbound HTTP client unconstrained by the advertised sandbox, to exfiltrate anything from (1) or (2) to an attacker-controlled endpoint;
  4. 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.

Options

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 and HttpURLConnection/URL POST to an attacker endpoint.
  • Data Store dead-drop - com.sap.it.api.asdk.datastore.DataStoreService put(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:

  1. HttpURLConnection calls 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.
  2. Deploy/modify an iFlow with a Request-Reply step targeting an on-prem-reaching destination.
  3. Bind that adapter’s credential_name (or a preceding Groovy SecureStoreService call) so CPI decrypts and injects the on-prem system’s own credentials - the attacker never needs to know them.
  4. 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.IntegrationDeveloper bundles 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.send governs 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/DeployIntegrationDesigntimeArtifact deploys - 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.
Note
Do 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.

Mitigation

  • Treat CPI Script-step design/deploy rights (AuthGroup.IntegrationDeveloper or 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/v1 design/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.

Detection and Monitoring

  • Script-step source containing ITApiFactory, SecureStoreService, KeystoreService, Runtime.getRuntime().exec, or raw HttpURLConnection/URL construction outside an approved adapter pattern - flag in code review / CPILint-style static scanning of transported iFlow content.
  • Deployment events (POST /api/v1/DeployIntegrationDesigntimeArtifact or 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.

References