Back to Blog
high SEVERITY2 min read

How a writable root filesystem service vulnerability happens in Docker Compose and how to fix it

The `opensearch` service defined in `Docker/LocalCluster.s3.yaml` was running with a fully writable root filesystem, giving any process inside the container the ability to modify binaries, drop payloads, or persist malicious changes. The fix pins the container to a read-only root filesystem with `read_only: true`, closing off a common post-compromise persistence and tampering vector while preserving normal OpenSearch operation.

O
By Orbis AppSec
Published September 7, 2026Reviewed September 7, 2026

Answer Summary

This is a Docker Compose security misconfiguration (CWE-732, Incorrect Permission Assignment for Critical Resource) where the `opensearch` service in `Docker/LocalCluster.s3.yaml` had a writable root filesystem, letting a compromised process modify container files or drop payloads. The fix adds `read_only: true` to the service definition, forcing any legitimate temporary writes to go through explicit `tmpfs` mounts or named volumes instead of the container's root filesystem.

Vulnerability at a Glance

cweCWE-732 (Incorrect Permission Assignment for Critical Resource)
fixAdd `read_only: true` to the service and mount `tmpfs`/named volumes for the specific paths OpenSearch legitimately needs to write to
riskContainer compromise leads to persistent payload storage, binary tampering, or malware download inside the running container
languageYAML / Docker Compose
root causeThe `opensearch` service definition in `Docker/LocalCluster.s3.yaml` omitted `read_only: true`, leaving the entire container filesystem writable by default
vulnerabilityWritable root filesystem in a Docker Compose service (opensearch)

Introduction

The Docker/LocalCluster.s3.yaml file defines the local development cluster used to spin up supporting services — including opensearch, which backs search and logging functionality for the stack. But a flaw in how that service was configured created a security risk: the opensearch container was allowed to run with a fully writable root filesystem, with no read_only restriction in place.

That might sound like a minor detail, but for anyone who has debugged a container compromise, it's a big deal. A writable root filesystem means that if an attacker ever gets code execution inside the opensearch container — through a vulnerable plugin, a deserialization bug, or a supply-chain compromise in a dependency — they don't just get a foothold, they get a durable one. They can drop tools, rewrite startup scripts, or tamper with application files, and those changes can persist for the life of the container.

The Vulnerability Explained

Semgrep's yaml.docker-compose.security.writable-filesystem-service rule flagged this exact pattern in Docker/LocalCluster.s3.yaml:

services:
  opensearch:
    image: opensearchproject/opensearch:2.11.0
    container_name: opensearch
    environment:
      - discovery.type=single-node
    ports:
      - "9200:9200"
    volumes:
      - opensearch-data:/usr/share/opensearch/data

Notice what's missing here: there is no read_only key at all. By default, Docker Compose services run with a fully writable filesystem unless you explicitly lock it down. That means every directory inside the container — /bin, /etc, /usr/share/opensearch, temp directories, everything — is writable by the OpenSearch process at runtime.

Why this matters in practice: OpenSearch, like most JVM-based services, exposes a large attack surface — REST APIs, plugin loading, script execution in some configurations, and JSON/YAML parsing of untrusted data. If any of that surface is ever exploited (for example, via a malicious query, a vulnerable plugin, or a poisoned dependency pulled in at build time), the attacker doesn't just get to read data — they get a container where they can:

  • Download and execute additional payloads (a second-stage backdoor, a cryptominer, a reverse shell binary).
  • Modify OpenSearch's own configuration or startup scripts so the compromise survives a container restart.
  • Tamper with logs or data files to cover their tracks.

Because the root filesystem is writable, none of this requires privilege escalation inside the container — the attacker already has write access to everything the OpenSearch process can reach.

The F

Frequently Asked Questions

What is a writable-filesystem-service vulnerability?

It's a container security misconfiguration where a Docker Compose (or Kubernetes) service definition doesn't restrict the container's root filesystem to read-only, allowing any process running inside — including a compromised one — to write, modify, or delete files anywhere in the container.

How do you prevent writable-filesystem-service issues in Docker Compose?

Set `read_only: true` on the service and explicitly grant write access only where needed, using `tmpfs` mounts for ephemeral data or named/bind volumes for persistent data, rather than leaving the entire container filesystem writable.

What CWE is writable-filesystem-service?

It maps most closely to CWE-732, Incorrect Permission Assignment for Critical Resource, since the container's filesystem permissions grant broader write access than the application actually requires.

Is a non-root container user enough to prevent this issue?

No. Running as a non-root user reduces privilege escalation risk but doesn't stop an attacker from writing to files the non-root user owns or has access to; `read_only: true` addresses the filesystem itself regardless of the running user.

Can static analysis detect writable-filesystem-service issues?

Yes. Static analysis tools like Semgrep can scan Docker Compose YAML files for missing `read_only: true` declarations on service definitions and flag them automatically, as was done here.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #291

Related Articles

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How dependabot-missing-cooldown happens in GitHub Actions/Node.js and how to fix it

The repository's `.github/dependabot.yml` had no cooldown period configured, meaning Dependabot could immediately propose updates to newly published package versions with zero time for the community to flag malware or instability. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` ecosystems, forcing a 7-day waiting period before new releases are surfaced as update PRs.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.

critical

How Remote Code Execution Happens in Handlebars Template Compilation and How to Fix It

CVE-2026-33937 is a critical remote code execution vulnerability in Handlebars.js that allows attackers to execute arbitrary code by passing maliciously crafted Abstract Syntax Tree (AST) objects to the compile() function. The vulnerability was patched in version 4.7.9, and we've upgraded to protect against this threat vector.

critical

How Denial of Service via Gzip Bomb happens in Node.js and how to fix it

A critical Denial of Service vulnerability (CVE-2026-59873) in the `tar` npm package allowed attackers to craft malicious gzip archives that could exhaust memory or CPU during decompression. The fix upgrades `tar` from 7.5.11 to 7.5.21 across `package.json` and `package-lock.json`, closing the resource-exhaustion path without changing any application code.