seggewiss

Learning IT security in public — eJPT, CPTS, OSCP, and what actually happens along the way.

Two Critical Bugs in Shopware's App System

In the last weeks I found two critical bugs in Shopware 6. Both are fixed now, and since 25 August 2026 both advisories are public. Both got accepted as critical GitHub Security Advisories, one scored 9.6, the other 9.1 on CVSS. And both break the same line in the system: the boundary between the shop and the Apps that run inside it.

I want to write down how I got there, without giving anyone a working exploit. The bugs are patched, but I keep the concrete details out on purpose.

Some context

Shopware has an App system. Apps are third-party extensions. The whole design assumes that an App is not fully trusted: it declares its required permissions in a manifest, it runs its logic in a restricted scripting layer, and the platform is supposed to keep it inside a sandbox. That sandbox is the interesting part. If a shop owner installs an App from the store, they trust the store, not the App author personally. So the sandbox has to hold. Especially now, with supply chain attacks, account takeovers and all that.

Bug 1 — App Scripts could break out of their sandbox

“App Script sandbox escape allows arbitrary PHP and OS command execution”

GHSA-6qhw-38wm-7g7h · CVSS 9.6 (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H) · also reported independently by @HDWSec

App Scripts let an App run logic at certain points in the shop. They should be sandboxed. An App author gets a restricted language, not the full PHP runtime, and definitely not the operating system.

How I got to this one is a bit stupid, and I like it for that. It started as a shower thought: why not use Lua for sandboxing the App System, instead of Twig? No plan behind it, only an idea about a different design. But to compare the two I had to know first what the current one really does. So the next step was simple: let me read how Shopware sandboxes Twig.

Cartoon of a person in the shower with shampoo foam on their head, thinking: why not just use Lua Sandbox?

My full research methodology. No plan, no budget, only shampoo.

After that it went fast.

Twig ships a sandbox extension, so that is what I looked for. It was nowhere. So the security layer has to be something else. And when I read what that layer does, the restrictions were a denylist.

That is the moment. You see “denylist” and you already feel how the story ends, you only have to prove it. It ended in remote code execution.

And that is the whole problem in one sentence: a sandbox built as a denylist is only as good as the list is complete. The restrictions could be bypassed. An App that was installed and activated could step out of the restricted layer and reach arbitrary PHP functions, and from there operating system commands, running as the web server process.

Once you are at “arbitrary OS command as the PHP process”, it is over for that host in the way that matters: read config and credentials, touch any file the process can write, reach internal services the shop can reach, or just take the shop down. GitHub classifies it as code injection (CWE-94), an incomplete list of disallowed inputs (CWE-184), and improper neutralization in a template engine (CWE-1336). That last one is exactly the “denylist is not a sandbox” point, written as a CWE.

Affected are shopware/core and shopware/platform from 6.5.4.0 up to 6.6.10.23, and from 6.7.0.0 up to 6.7.13.1.

One more thing about the credits on this advisory. @HDWSec is named there next to me, and I want to be clear what that means: they found the same issue on their own, around the same time. It was not joint work, we did not research it together. We reported it separately. I name them here because they deserve the credit for finding it, not because we worked as a team.

Bug 2 — a custom entity name that was really SQL

“Stored SQL Injection via Custom Entity Field Names (App Manifest)”

GHSA-xrcf-c96g-q5hr · CVSS 9.1 (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H)

The second one is my favourite of the two, because it was not what I expected.

Bug 1 I found by accident, out of a shower thought. This one I was looking for, at least a bit. I went through the code with one pattern in my head: where does a variable, or the result of a function call, land directly inside an SQL statement as text? Not bound as a parameter, not in a prepared statement. Really written into the string. That is the whole search. It is not clever, it is boring and you repeat it a lot. And that is how I found this one.

Apps can define custom entities in their manifest, so basically their own database tables, described in the App’s configuration. Shopware reads that description and creates the storage for it. The names in that description, entity names and field names, are identifiers.

Identifiers are the classic blind spot for SQL injection. Everybody parametrizes values now. The ? in a prepared statement handles the value in a WHERE clause. But you can not bind a table name or a column name as a parameter, those get built into the statement as text. So if those identifiers come from an App manifest and are not strictly validated, an App can put SQL where the platform expected a plain name, and it runs with the database connection’s full rights. Not only reading and writing rows, also changing the schema itself (that is the “DDL” in the advisory title). GitHub tags it improper input validation (CWE-20) and SQL injection (CWE-89).

Affected here is the 6.7 line from 6.7.0.0 up to 6.7.13.1, plus everything below 6.6.10.23.

The reason I like it: there is no payload that looks scary. It is a field name. It sits in a config file. It only becomes an injection because of where the name is allowed to travel and how little it was checked on the way. That is the kind of bug you only see if you keep asking the trust question: an attacker controls this string, so where does this string end up?

Why both of these are a big deal

Look at what an attacker needs. No network position, no stolen admin session in the classic sense. An App. The Shopware store is a marketplace. A shop installs Apps. That is the supply chain, and both of these bugs turn “you installed an App” into either full command execution on the server or full control of the database.

  • Bug 1: a malicious or compromised App gets arbitrary code on the server.
  • Bug 2: a malicious or compromised App gets arbitrary SQL on the shop’s database.

Both advisories put the scope as changed (S:C in the vector), which is the formal way of saying the damage does not stay inside the sandbox. That is the whole point of both findings.

“Compromised” matters as much as “malicious”. A well-meaning App whose author’s account or build pipeline gets taken over becomes the delivery vehicle. That is why the fix belongs in the platform and not in “only install good Apps”.

Responsible disclosure

I reported both through Shopware’s security process. Both were accepted as critical advisories and fixed in the platform:

  • Fixed in 6.6.10.23 and 6.7.13.1. If you run a Shopware 6 shop, update. That is the whole action item.

Both advisories were published on 25 August 2026:

No CVE IDs were assigned to either one, so the GHSA IDs are the identifiers to use. There is no proof-of-concept in this article and there will not be one. The point is the boundary, and you do not need my exploit to understand the boundary or to patch.

The people on the Shopware side handled it well and fast, and I want to say that plainly. Reporting a bug into the company I work with could have been awkward. It was not. That says something good about the security culture there.

What I took from it

Two things.

A sandbox is a promise, and a denylist is not a sandbox. If the design is “everything is allowed except this list”, then the security is a race between the list and everyone who reads the code looking for what is not on it. Bug 1 is that race, lost.

Follow the string, not the value. The SQL injection was not in a value that someone forgot to escape. It was in a name that nobody thought of as attacker input, because it lives in a config file and looks harmless. The habit that found it is boring and it works: pick a piece of data an attacker controls, and follow it through the code until it does something dangerous or you are sure it can not.

Both bugs came from the same question, asked over and over: where does this code decide to trust something? Keep asking those questions.