The Configuration Trap: How Enterprise Solutions Become SaaS Vulnerabilities

 · 5 min read

After deciding to split the product into Enterprise and SaaS, we started designing the cloud architecture. During the planning phase, the task seemed straightforward: spin up an API, configure container-workers in the cloud, and simply pass the code to them for processing.

Zero Trust Cloud Config Trap

However, during a detailed security audit, we discovered that architectural patterns which worked flawlessly in a trusted corporate environment turn a public SaaS into an open hacking sandbox.

Anatomy of a Vulnerability: RCE via Configuration

RCE Vulnerability in Config

In the Enterprise version, the configuration logic was maximally flexible: the engine simply read the flude_*_config.json configuration file directly from the client’s repository.

The problem lay in exactly how these parameters were translated into parser settings under the hood. Our pipeline relied on Doxygen, which we extended with our own scripts to bypass C++ bugs. To make these filters work, the engine uses a standard Doxygen directive—INPUT_FILTER, which allows executing arbitrary shell commands for processed files.

In an isolated client perimeter, reading settings from the repository was absolutely safe. But when transferring this logic to a public cloud (GCP Cloud Run), a critical vector for Remote Code Execution (RCE) emerged. If a server blindly trusts a flude_*_config.json file from a third-party repository, an attacker can pass through overrides for parser parameters. By injecting a command like rm -rf / or a reverse-shell via INPUT_FILTER substitution, an attacker would force our system to legitimately execute this code on our own servers.

Zero-Trust Architecture and Breaking the Trust

Zero-Trust Architecture

Realizing this risk forced us to completely rethink our approach to configuration. In a public cloud, you cannot trust a single line coming from the outside. We implemented a strict Zero-Trust principle:

  1. Abandoning Client Configurations: The SaaS version will never read flude_*_config.json from a user’s repository.
  2. Server Templates: Our cloud worker operates exclusively on hardcoded, versioned server templates for Doxyfiles and configurations.
  3. Secure Parameter Passing: A user can only pass a strictly limited set of safe data-parameters (e.g., directories to include or exclude) via the API, which are rigidly validated on the Control Plane side. Control over which tools and filters run in the container has been permanently handed over to the server.

Fortifying the Perimeter: SSRF and Workload Identity

Having plugged the RCE hole, we turned our attention to network security. Cloning user repositories by a server is a classic Server-Side Request Forgery (SSRF) risk. To prevent attackers from forcing our workers to scan the internal GCP network under the guise of a git repository, we implemented a strict Allowlist: cloning is allowed only via HTTPS and strictly from the domains github.com, gitlab.com, and bitbucket.org.

Additionally, to minimize damage in the hypothetical event of a container breach, we completely abandoned static keys (Service Account Keys), transitioning all workers to dynamic authentication via GCP Workload Identity Federation.

Deferred Threats: Attack via Source Code

Having secured the configuration and network, we asked the next question: what if the attack vector becomes the very source code we are parsing?

We analyzed several highly specific scenarios:

  1. Doxygen Tag Abuse: An attacker could write standard tags like \include, \verbinclude, or \htmlinclude into a code comment with a path like ../../../etc/passwd. These will force the parser to read a file outside the project directory and insert its contents into the final documentation, which is then returned to the client. Our strict directory validation is powerless here, as it only checks the declared input paths, not what Doxygen decides to read during the parsing process.
  2. Malicious Symlinks: If an attacker commits a symlink pointing to system files of the container, and the parser follows it while traversing directories—this is another way to extract data outside the repository sandbox.
  3. Denial of Service (DoS): In our system, the call to the Doxygen binary is limited to 60 seconds, but the parent flude_engine compile process had no timeout. A specially crafted source file (with infinite template recursion or explosive macro expansion) could permanently hang the cloud worker process.

After considering these vectors, we made a conscious decision to defer fixing them.

First, architecturally it was unclear at what level to implement these defenses—especially in light of the upcoming introduction of native parsers. We honestly recorded this debt in the project roadmap under the “Input Validation & Sanitization” section.

Second, the actual risk was significantly reduced thanks to the Data Plane architecture. All workers in Cloud Run are isolated via gVisor, and the containers themselves are absolutely ephemeral. Even in the event of a successful exploit of a symlink or an \include tag, an attacker would read files from a disposable, sterile environment without access to neighboring tasks or the host.

The Accumulating Dissatisfaction with Doxygen

that the incidents with potential RCE via INPUT_FILTER and the vulnerability of standard tags like \include became more than just cloud security issues. They were another wake-up call reminding us of Doxygen’s fundamental architectural flaws. Its inflexibility, reliance on shell calls, and poor performance made it a burden for a scalable SaaS solution (and for the architecture as a whole).

Running ahead, we can say: we did not put up with this technical debt. In a couple of weeks, as soon as we finish the series of articles on SaaS infrastructure, we will explain in detail why we finally fired Doxygen.

But before that, we had to solve another pressing cloud problem. We had designed a secure Data Plane, protected from hackers, but now we needed to protect it from bankruptcy. How we built a Free-Tier, protected the API from abuse loads, and designed billing—read about it in the next episode.