Please use the GitHub Security Advisory "Report a Vulnerability" tab. Do not open a public issue for a security report.
Expect an acknowledgement within a few days. This project is developed alongside other work, so a fix is agreed rather than promised by a date — the advisory is where that conversation happens.
Only the latest published version of @abap2ui5/mcp-server is supported.
It is still on the 0.x line, so a fix ships as the next release rather than
as a patch to an older line.
Worth knowing before assessing a report:
- It is a local stdio server, not a network service. The transport is
StdioServerTransport— it speaks to the MCP client that started it over its own stdin and stdout. It opens no socket and listens on no port, so it has no attack surface reachable from another machine. The backend it starts forrun_appand the app tools is a child listening on 127.0.0.1 only - the npm host binds loopback itself, and a framework checkout'snode/srv/express.mjs(which binds every interface withoutHOST) is started withHOST=127.0.0.1. Loopback is reachable from every browser page on the machine, though, so the npm host also answers only requests addressed to127.0.0.1,localhostor[::1](theHostheader) and, when they carry anOrigin, sent from a page there - a page cannot drive or read the dev apps through DNS rebinding. The server's own clients of that backend (the start's port wait,run_app's Chromium, the app tools) connect to127.0.0.1itself, never to the namelocalhost, which can resolve to::1- a port another local user may hold. A framework checkout'sexpress.mjschecks neither header; that check belongs to the framework. - A default directory under the shared temp dir is used only when it is
the user's own. The GitHub mirror's default base,
<tmp>/abap2ui5-mcp-remote, and the default screenshot dir,<tmp>/abap2ui5-mcp-screenshots(the PNGs ofrun_appandinteract_app, and the build logbuild_logreads after a restart), have fixed names underos.tmpdir()-/tmpon Linux, which every local user can write. Each is read and written only when it is a real directory (no symbolic link), owned by the user running the server and writable by nobody else; the server creates it 0700 and narrows one of its own that others can read (lib/private-dir.mjs). Anything else is refused with the reason - a screenshot is then returned but not saved - andA2UI5_MCP_REMOTE_DIR/A2UI5_MCP_SCREENSHOT_DIRput them elsewhere (a directory those variables name is not checked - below). - A directory the operator names is trusted like the operator's shell.
A2UI5_MCP_REMOTE_DIR,A2UI5_MCP_SCREENSHOT_DIRandA2UI5_MCP_WORKSPACE(and a checkout env var such asA2UI5_HOME) are used as they are: no owner, mode or symbolic-link check, because the person who set the variable chose the place, and refusing a shared team directory or a symlinked home they set up on purpose would be the server second-guessing its operator. What follows from that is the operator's to keep true: whoever can write into such a directory decides what the server serves and runs. The mirror directory is what the knowledge tools answer with and whatadd_agent_setupcopies into a project; the workspace holds thenode_modulesthe backend, the transpiler and abaplint run from; the screenshot directory receives the pictures of the user's apps and the build logbuild_logreads back. Point them at directories only you can write - never at a world-writable place such as/tmpitself. The defaults are checked (above) precisely because nobody chose them. - A child that runs the app gets an allowlisted environment. The
backend and the unit-test runner execute the transpiled ABAP, and
open-abap's
@KERNELescape gives that codeprocess.env. They inherit only what Node, the runtime and an app's outbound HTTP need -PATH, the home and temp directories (HOME,USERPROFILE,TMPDIR,TMP,TEMP),TZand the locale (LANG,LANGUAGE,LC_*),NODE_ENVandDEBUG(express reads them),NODE_OPTIONS,NODE_ICU_DATA, the CA and TLS variables (NODE_EXTRA_CA_CERTS,SSL_CERT_FILE,SSL_CERT_DIR,OPENSSL_CONF, andLD_LIBRARY_PATHfor the libraries they load), the proxy variables (HTTP_PROXY,HTTPS_PROXY,NO_PROXY,ALL_PROXY,NODE_USE_ENV_PROXY- a proxy URL may carry its credentials: it is passed because an app's HTTP needs it as much as the server's), the Windows system variables (SYSTEMROOT,WINDIR,COMSPEC,PATHEXT,SYSTEMDRIVE,APPDATA,LOCALAPPDATA,USERNAME,USERDOMAIN,PROCESSOR_ARCHITECTURE,NUMBER_OF_PROCESSORS) - plusPORTand the loopbackHOST(appChildEnvinlib/runtime.mjslists each with its reader;test/security.test.mjsfails when this list and that one differ). The GitHub token, the system-mode SAP credentials and anything else the user's shell exports (cloud keys, registry tokens) stay with the server. Defence in depth: deploying ABAP is running code with your privileges, and an allowlist does not change that. The build, lint, git and npm children run no app code and keep the full environment their own configuration lives in. - A checkout's symbolic links are not followed out of it. A checkout is
untrusted content - a sample, docs or template repository cloned from
GitHub, a sandbox an earlier deploy filled - and a repository can ship a
symbolic link (
docs/agents/building-apps.md -> ~/.ssh/id_rsa). Every file a tool answers with or copies (the guide, the interface, the pitfalls, the capability map, the porting brief, the sample catalogues, the docs tree, a sampleread_examplereads, the template filesscaffold_appserves andadd_agent_setupcopies into a project, the support classesmigrate_report { deploy: true }copies, the sandbox the npm build transpiles) is read only when it resolves inside its checkout, and every write into the sandbox or a checkout is refused when its path leaves it - a dangling link included, since writing through one creates its target wherever it points - and opened withO_NOFOLLOW, so a link that appears after the check fails the write instead of redirecting it (lib/contain.mjs). Checkouts whose CODE this server runs anyway - the framework (its backend), samples-controls' scripts (scope_of, the full build), abap-cloud-gui's converter (imported in-process), app-template'sscripts/check-pin.mjs- are trusted the way that code is; the link checks still apply to what is read from them. migrate_reportrefuses a report that carries the transpiler's code escape. On an SAP systemWRITE '@KERNEL <text>'.writes a line; the transpiler copies<text>into the generated module as JavaScript. No classic report has a reason to contain it, so the tool refuses such a source (and any converter output that contains one) before anything is converted or written. A dynamicLOOP AT ... WHERE (<condition>)is ordinary ABAP and is converted, but flagged:@abaplint/runtimeevaluates the condition witheval(), so one built from user input is code injection on the backend - an upstream property of the runtime this server reports rather than fixes.- It runs with the privileges of whoever started it, over the checkouts
beside it, and the expensive half of its tool loop (
build_backend,run_app) executes code: it transpiles the ABAP and boots the resulting app. A tool call is therefore as trusted as the repository it is pointed at. Treat an MCP client that can reach this server the way you would treat a shell in the same directory. - What it downloads from GitHub is capped in size, not only in time.
A file of the read-only mirror, the docs tree listing and the framework's
release list are read up to 8 MB (the largest is 0.34 MB today), the
prebuilt backend archive up to 200 MB (2.7 MB today) - so a proxy or
mirror in between that answers with a huge or endless body cannot fill
the memory or the temp disk. An answer that declares more is refused
unread, one that streams more is cut at the cap; either is reported by URL
(
TEXT_MAX_BYTESinlib/remote.mjs,PREBUILT_MAX_BYTESinlib/runtime.mjs). - Every spawned child gets a hard timeout and is killed as a process group
(
lib/runtime.mjs), so a hung or forking build cannot outlive the call that started it. - It runs only tools the checkouts installed themselves. The lint and
the incremental transpile start
<checkout>/node_modules/@abaplint/cliand@abaplint/transpiler-clidirectly. They used to go throughnpx, which — with no local bin and no TTY to answer its prompt — fetched whatever the registry held under that name (abap_transpileis not a claimed package name there). A missing install is now reported with thenpm cithat fixes it; nothing is downloaded to be executed. - What the npm backend installs, and how. Without a framework checkout
the expensive half installs
@abap2ui5/node-runtime(the release the registry names as latest, orA2UI5_MCP_RUNTIME_VERSION),expresswithin that package's peer range,@abaplint/transpiler-cliat the version the package records and@abaplint/cliat app-template's pin - four direct dependencies, about 76 packages in all, where the framework clone'snpm ciinstalled about 205. They go into a directory of their own under~/.abap2ui5-mcp/runtime/<version>, through the user's own npm and its registry configuration (the user-level config andnpm_config_*; the registry lookup and the install both run in that workspace, so a project.npmrcwhere the server was started is read by neither), with--ignore-scripts(no install script of any dependency runs), the exact versions recorded (--save-exact) and apackage-lock.jsonwhose integrity hashes npm checks on every reinstall. Versions and commits read from the registry are validated (a plainX.Y.Z, a 40-character sha) before they become a path or an argument, and npm runs without a shell except on Windows, where it is a.cmdscript and its arguments are quoted. open-abap-core is fetched with git at the exact commit the release records (git fetch --depth 1 <sha>, the checkout'srev-parseverified before it is used) - never a branch, except for a release that records none, which gets the default branch's HEAD resolved to a sha and says so. The backend it boots listens on 127.0.0.1 only. What runs is still code from npm and GitHub: the package's transpiled framework in the backend process, the transpiler and abaplint during a build and a lint. Trusting them is trusting the abap2UI5 and abaplint publishers, as the clone'snpm cidid - over far fewer packages. - The cheap half never executes what it reads.
validate_viewandscreenshot_viewwork from source through the linter's render harness: the ABAP is parsed and the reconstructed view is loaded in headless Chromium. That browser step does run markup — reconstructed, not fetched — so treat it as you would any build step over untrusted input. - It is published with provenance. Releases go out from
.github/workflows/release.ymlthrough npm trusted publishing (OIDC), so there is no long-lived npm token in this repository to leak, and every published tarball carries an attestation linking it to the commit and workflow that built it. Verify withnpm audit signatures.
- What a tool reports about your ABAP or your views — that is the product, not a vulnerability. Open an issue.
- A wrong or missing finding from the linter behind
validate_view. That belongs in abap2UI5/linter.