Skip to content

Latest commit

 

History

History
144 lines (109 loc) · 5.54 KB

File metadata and controls

144 lines (109 loc) · 5.54 KB

Exploit chains

A scanner that looks at one artifact at a time reports findings one at a time. But the risk in a multi-service stack is often in the relationships between services, not in any single one of them.

A database with a committed password is one finding. A database published on 0.0.0.0 is another. A database that is both, and shares a network with an internet-facing service, is a single exploitable path that neither finding describes on its own.

DockSec detects these paths and reports them as chains: the services involved, the findings that combine, the attack path in order, and the one change that breaks it most effectively.

Why this is rule-based, not AI-based

Chain detection is a graph query over the compose topology - what is exposed, what holds a credential, what can reach what, what has already escaped the container boundary. That is a deterministic question, so it is answered deterministically.

The practical consequences:

  • Chains are reported with --scan-only, offline, and with no API key.
  • The same stack always produces the same chains.
  • A CI gate can depend on them.

The AI correlation pass adds a layer on top - ranking chains against each other, catching shapes the rules do not cover, and explaining impact in the context of a specific application. It is not load-bearing for the detection itself.

The chains DockSec detects

Exposed credentialed datastore

Severity: CRITICAL

A database, cache, or broker publishes a port on all interfaces and its credential is a literal value in the compose file.

services:
  db:
    image: postgres:16
    ports:
      - "5432:5432"                        # binds 0.0.0.0
    environment:
      POSTGRES_PASSWORD: hunter2           # committed to the repository

No application vulnerability is required. Anyone who can route to the host and read the repository can connect to the datastore and authenticate. On a cloud VM with a public address, that is the internet.

Breaks with: binding to 127.0.0.1 (or dropping the published port and using expose), and moving the credential to a Docker secret. Either alone reduces the severity; both remove the chain.

Exposed escape path

Severity: CRITICAL

A service that publishes a port also mounts the Docker socket or runs privileged.

services:
  web:
    image: nginx
    ports:
      - "80:80"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

The service is reachable from outside the host, and the container boundary it sits behind is already open. Remote code execution in this service is a host compromise rather than a contained one - the usual "it is only a container" mitigation does not apply.

Breaks with: removing the socket mount or the privileged flag. That is the change that matters; closing the port just makes the escape path harder to reach.

Lateral movement to a datastore

Severity: HIGH

An internet-facing service shares a network with a credentialed datastore that is not itself exposed.

services:
  web:
    image: nginx
    ports:
      - "80:80"        # internet-facing
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: hunter2
    # no ports, no networks - so it joins the default network, with web

This is the chain that per-service review misses most often. db is not exposed, so it looks contained. web has no credential, so it looks low-value. But compose puts both on the default network when neither declares one, so compromising web yields authenticated access to db.

Breaks with: putting the datastore on its own network that the exposed service does not join, or moving the credential to a secret so a compromise of the front-end does not hand over the password.

Reading chain output

Exploit chains
  [CRITICAL] PostgreSQL on 'db' is reachable from any host and its credential is in the compose file
      services: db
      combines: compose-port-bound-all-interfaces, compose-plaintext-secret-env
      Port 5432 is published with no host IP, so it binds 0.0.0.0 and accepts connections from
      outside the host. The credential in POSTGRES_PASSWORD is committed alongside it, so anyone
      who can read the repository can authenticate to the datastore directly - no application
      vulnerability required.
      break it: Bind the port to 127.0.0.1 (or drop it and use 'expose'), and move
      POSTGRES_PASSWORD to a Docker secret.
  • services - which services take part. A chain spanning two services is one no per-service view would surface.
  • combines - the individual rule IDs. Each is documented in the compose rule reference.
  • break it - the single most effective change. Chains usually have several links; this names the one worth doing first.

Chains also appear in --json under exploit_chains, so CI can act on them:

docksec --compose docker-compose.yml --scan-only --json \
  | jq '.exploit_chains[] | select(.severity == "CRITICAL")'

What is not yet detected

Stated plainly, because an empty chain list should not be read as "no chains exist":

  • Chains that span a Dockerfile and a compose file (a vulnerable package in an image that a chain then reaches).
  • Chains involving Kubernetes or Helm resources - compose only, for now.
  • Reachability: whether the vulnerable code path in an exposed service is actually invoked.
  • Trust relationships expressed outside the compose file, such as a firewall or a service mesh policy. A stack protected by one may still report a chain, which is a reasonable case for a waiver.