Is SAP CPI Helper safe?

Low risk

SAP CPI Helper is low risk. SAP CPI Helper's GroovyDebugX action confirms, then opens a groovyide.com URL: a raw-DEFLATE, base64url JSON package built from trace data, which can include message body, runtime headers, exchange properties, and Groovy script source.

dbeckbauerv3.25.0Chrome Web Store
20Risk

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

SAP CPI trace data is packed into a groovyide.com share URL

SAP CPI Helper's GroovyDebugX action confirms, then opens a groovyide.com URL: a raw-DEFLATE, base64url JSON package built from trace data, which can include message body, runtime headers, exchange properties, and Groovy script source.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You click the GroovyDebugX option to debug a traced SAP CPI Groovy step in an external IDE.

The confirmation dialog lets you choose message body, script, properties, and headers before continuing.

The extension did this

The extension creates a compressed share link and opens groovyide.com in a new tab.

The share link carries the selected trace fields inside the URL path rather than a separate request body.

02EvidenceFIELD TABLE
Concrete JSON fields placed into the share package
FieldValueWhy it matters
Message body
input.body = payload from TraceMessages(traceId)/$valueCan include the business payload from the SAP CPI trace you are debugging.
Runtime headers
input.headers = {"Content-Type":"application/json"}Can reveal routing, integration, and message-handling context for the traced exchange.
Exchange properties
input.properties = {"CamelSplitIndex":"0"}Can include configuration and runtime values attached to the SAP CPI exchange.
Groovy script source
script.code = "import com.sap.gateway.ip.core.customdev.util.Message\nMessage processData(Message message) { return message }"Can disclose the integration logic you are trying to debug.
Function name
script.function = "processData"Identifies which Groovy entry point the external IDE should run for the debug session.
03EvidenceNETWORK CAPTURE
Captured request
GEThttps://groovyide.com/cpi/share/v1/
The extension opens this default share endpoint with the encoded trace package appended to the path; no request body was recorded for this claim.
04EvidenceCODE COMPARE
The code that does this

The confirmation dialog and sender that build the groovyide.com URL

What it actually does
Readable confirmation workflow from the deobfuscated sourceplugins/groovyDebugger.js
window.groovyDebugSendToIDE = async function () {
  const settings = window.groovyDebuggerData?.settings || {};
  const ideUrl = settings["groovyDebugger---externalIdeUrl"]  || "https://groovyide.com/cpi/share/v1/";
  const domain = new URL(ideUrl).hostname;

  // Load saved user preferences for checkbox states using plugin settings
  const loadTransferPreferences = async () => {
    const body = (await getStorageValue("groovyDebugger", "transferBody", "browser")) ?? true;
    const script = (await getStorageValue("groovyDebugger", "transferScript", "browser")) ?? true;
    const properties = (await getStorageValue("groovyDebugger", "transferProperties", "browser")) ?? false;
    const headers = (await getStorageValue("groovyDebugger", "transferHeaders", "browser")) ?? false;

    return {
      body: body,
      script: script,
      properties: properties,
      headers: headers,
    };
  };

  // Load saved user preferences for checkbox states
  const userPreferences = await loadTransferPreferences();

  // Create custom confirmation popup
  const popupContent = `
    <div class="ui warning message">
      <div class="header">
        <i class="exclamation triangle icon"></i>
        Data Transfer Confirmation
      </div>
      <div class="ui info message">
        <i class="info circle icon"></i>
        <strong>Privacy Notice:</strong> The selected data may contain sensitive business information. Proceed with caution.
      </div>
      <p><strong>Destination:</strong> ${domain}</p>
      <p>Select which data you want to transfer to the external Groovy WebIDE:</p>
      <div class="ui form">
        <div class="grouped fields">
          <div class="field">
            <div class="ui checkbox" id="transfer-body">
              <input type="checkbox" name="transfer-body" ${userPreferences.body ? "checked" : ""}>
              <label>Message Body <em>(may contain sensitive data)</em></label>
            </div>
          </div>
          <div class="field">
            <div class="ui checkbox" id="transfer-script">
              <input type="checkbox" name="transfer-script" ${userPreferences.script ? "checked" : ""}>
              <label>Groovy Script <em>(source code)</em></label>
            </div>
          </div>
          <div class="field">
            <div class="ui checkbox" id="transfer-properties">
              <input type="checkbox" name="transfer-properties" ${userPreferences.properties ? "checked" : ""}>
              <label>Properties <em>(may contain configuration data)</em></label>
            </div>
          </div>
          <div class="field">
            <div class="ui checkbox" id="transfer-headers">
              <input type="checkbox" name="transfer-headers" ${userPreferences.headers ? "checked" : ""}>
              <label>Headers <em>(may contain security & metadata)</em></label>
            </div>
          </div>
        </div>
      </div>
    </div>
  `;

  showBigPopup(popupContent, "Confirm Data Transfer", {
    fullscreen: false,
    large: false,
    callback: () => {
      // Add custom buttons to the actions div
      let actionsDiv = $("#cpiHelper_semanticui_modal .actions");
      actionsDiv.empty(); // Remove default close button

      // Cancel button
      let cancelBtn = $('<div class="ui button">Cancel</div>');
      cancelBtn.on("click", () => {
        $("#cpiHelper_semanticui_modal").modal("hide");
      });
      actionsDiv.append(cancelBtn);

      // Continue button
      let continueBtn = $('<div class="ui positive button"><i class="rocket icon"></i>Continue</div>');

      // Function to check if any checkbox is selected and enable/disable continue button
      const updateContinueButton = () => {
        const anyChecked = $("#transfer-body input").is(":checked") || $("#transfer-properties input").is(":checked") || $("#transfer-headers input").is(":checked") || $("#transfer-script input").is(":checked");

        if (anyChecked) {
          continueBtn.removeClass("disabled");
          continueBtn.prop("disabled", false);
        } else {
          continueBtn.addClass("disabled");
          continueBtn.prop("disabled", true);
        }
      };

      // Add event listeners to all checkboxes to update continue button state
      $("#transfer-body input, #transfer-properties input, #transfer-headers input, #transfer-script input").on("change", updateContinueButton);

      // Initial check
      updateContinueButton();

      continueBtn.on("click", async () => {
        // Get selected data types
        const transferOptions = {
          body: $("#transfer-body input").is(":checked"),
          properties: $("#transfer-properties input").is(":checked"),
          headers: $("#transfer-headers input").is(":checked"),
          script: $("#transfer-script input").is(":checked"),
        };

        // Save user preferences for next time using plugin settings
        await syncChromeStoragePromise(getStoragePath("groovyDebugger", "transferBody", "browser"), transferOptions.body);
        await syncChromeStoragePromise(getStoragePath("groovyDebugger", "transferScript", "browser"), transferOptions.script);
        await syncChromeStoragePromise(getStoragePath("groovyDebugger", "transferProperties", "browser"), transferOptions.properties);
        await syncChromeStoragePromise(getStoragePath("groovyDebugger", "transferHeaders", "browser"), transferOptions.headers);

        $("#cpiHelper_semanticui_modal").modal("hide");
        const debugData = window.currentGroovyDebugData;
        await sendToExternalIDE(settings, debugData, transferOptions);
        showToast(`Debug data sent to IDE`, "Success");
        // Close the main debug popup after sending
        $("#cpiHelper_semanticui_modal").modal("hide");
      });
      actionsDiv.append(continueBtn);
    },
  });
}
Readable sender and encoder from the deobfuscated sourceplugins/groovyDebugger.js
async function sendToExternalIDE(settings, debugData, transferOptions = { body: true, properties: true, headers: true, script: true }) {
  var ideUrl = settings["groovyDebugger---externalIdeUrl"]  || "https://groovyide.com/cpi/share/v1/";

  // Use actual debug data based on transfer options
  let groovyScript = "";
  if (transferOptions.script) {
    groovyScript = debugData.groovyScript;
    // If script not fetched yet (lazy loading), fetch it now
    if (groovyScript === "// Script content not available" && debugData.scriptInfo) {
      try {
        if (debugData.scriptInfo.scriptPath) {
          let scriptPath = debugData.scriptInfo.scriptPath;
          if (scriptPath.startsWith("/script/")) {
            scriptPath = scriptPath.replace("/script/", "//");
          }
          const scriptUrl = "https://" + debugData.scriptInfo.tenant + "/api/1.0/iflows/" + debugData.scriptInfo.artifactId + "/script/" + scriptPath;
          const scriptResponse = await fetch(scriptUrl);
          const scriptData = await scriptResponse.json();
          groovyScript = scriptData.content || "// Script content not available";
        }
      } catch (error) {
        log.error("Error fetching script for IDE:", error);
        groovyScript = "// Script content not available";
      }
    }
  }

  let payload = "";
  if (transferOptions.body) {
    payload = debugData.payload;
    // If payload not fetched yet (lazy loading), fetch it now
    if (!payload && debugData.traceId) {
      try {
        payload = await makeCallPromise("GET", "/" + cpiData.urlExtension + "odata/api/v1/TraceMessages(" + debugData.traceId + ")/$value", true);
      } catch (error) {
        log.error("Error fetching payload for IDE:", error);
        payload = "";
      }
    }
  }

  let headers = {};
  if (transferOptions.headers) {
    headers = debugData.headers || {};
    // If headers not fetched yet (lazy loading), fetch them now
    if ((!headers || Object.keys(headers).length === 0) && debugData.traceId) {
      try {
        let headersData = JSON.parse(await makeCallPromise("GET", "/" + cpiData.urlExtension + "odata/api/v1/TraceMessages(" + debugData.traceId + ")/Properties?$format=json", true)).d.results;
        headers = {};
        headersData.forEach((header) => {
          headers[header.Name] = header.Value;
        });
      } catch (error) {
        log.error("Error fetching headers for IDE:", error);
        headers = {};
      }
    }
  }

  let properties = {};
  if (transferOptions.properties) {
    properties = debugData.properties || {};
  }

  // Build the JSON structure from actual debug data
  let dataObject = {
    input: {
      body: payload,
      headers: headers,
      properties: properties,
    },
    script: {
      code: groovyScript,
      function: debugData.scriptFunction || "processData",
    },
  };

  let dataString = JSON.stringify(dataObject);

  // Compress and encode
  let encoded;
  encoded = await compressToBase64(dataString);
  // Make URL-safe and remove padding
  encoded = encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");

  var fullUrl = ideUrl + encoded;

  // Open in new tab/window
  window.open(fullUrl, "_blank");
}

function compressToBase64(dataString) {
  // Step A: Convert the JSON string into a Uint8Array (binary data)
  const dataBytes = new TextEncoder().encode(dataString);

  // Step B: Compress using pako.deflateRaw()
  // This creates the raw Deflate stream without Zlib/Gzip headers.
  const compressedBytes = pako.deflateRaw(dataBytes, { level: 9 }); // level 9 is max compression

  // Step C: Base64URL Encode the compressed binary data
  const encodedString = uint8ArrayToBase64Url(compressedBytes);

  return encodedString;
}

function uint8ArrayToBase64Url(bytes) {
  // Convert Uint8Array to a binary string
  let binaryString = "";
  bytes.forEach((byte) => {
    binaryString += String.fromCharCode(byte);
  });

  // Standard Base64 encoding using the built-in browser function
  let base64 = btoa(binaryString);

  // Convert to URL-safe format and remove padding
  let base64Url = base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");

  return base64Url;
}
05EvidenceTHIRD PARTY LIST
Remote destination used by the external IDE workflow
  • groovyide.com

    Default Groovy WebIDE share destination that receives the encoded SAP CPI trace package in the URL path.

Updated 17 September 2026epoggeaemnkacpinjfgccbjakglngkpb