ExZipProtect

View Source

Module Version Hex Docs Total Download License Last Updated

Protect your Phoenix/Plug applications from low‑effort scrapers, scanners, and spam bots by serving them compressed "zip bombs"—ultra‑small payloads that expand to hundreds of MB/GB and exhaust their memory. ExZipProtect does zero detection itself; it simply makes it trivial for you to return a pre‑built bomb when your own heuristics say, “Nuke this request.”


✨ Features

FeatureDetails
BYOBBring Your Own Bomb—the library never ships with or generates payloads. You point to files you already built (gzip / zstd / brotli, etc.).
Multiple storage back‑ends{:file, path} · {:s3, bucket: …, key: …} · {:url, "https://…"}
Rotation (opt‑in):none (default), :random, or :round_robin across multiple bombs per severity level.
Staging safe‑guardEntire library can be disabled with a single flag (default off in non‑prod envs).
Enable bypass headerConfigure an optional HTTP header to serve as bypass to ExZipProtect (useful for security scanners).
Telemetry audit eventEmits [:ex_zip_protect, :bomb, :served] so you can track activations.
Mix generatormix ex_zip_protect.gen.config scaffolds a ready‑to‑edit config file.

📦 Installation

Add to mix.exs:

defp deps do
  [
    {:ex_zip_protect, "~> 0.1"},

    # OPTIONAL — required only if you use these source types:
    {:ex_aws, "~> 2.5", optional: true}
    {:ex_aws_s3, "~> 2.5", optional: true},   # for :s3 sources
    {:finch,     "~> 0.18", optional: true}    # for :url streaming if not already in deps
  ]
end

Run mix deps.get.


🔧 Quick start

$ mix ex_zip_protect.gen.config   # creates config/ex_zip_protect.exs
$ $EDITOR config/ex_zip_protect.exs   # edit paths / bucket names / rotation

Then in a controller or plug:

alias ExZipProtect.Plug, as: Bomb

plug :maybe_bomb

defp maybe_bomb(conn, _opts) do
  case Detection.classify(conn) do
    {:bomb, :low   } -> Bomb.send(conn, :low)
    {:bomb, :medium} -> Bomb.send(conn, :medium)
    {:bomb, :high  } -> Bomb.send(conn, :high)
    _other           -> conn
  end
end

Done! Your normal pipeline proceeds unless you explicitly call Bomb.send/2.


⚙️ Configuration reference (config/*.exs)

import Config

config :ex_zip_protect,
  enabled?: config_env() == :prod,   # disable library outside prod by default
  rotation: :none,                  # :none | :random | :round_robin
  levels: [
    low:    [ %{source: {:file, "/srv/bombs/low.gz"},    encoding: :gzip} ],
    medium: [ %{source: {:s3, bucket: "bombs", key: "5mb.zst"}, encoding: :zstd} ],
    high:   [
      %{source: {:url, "https://cdn.example.com/10mb.br"}, encoding: :br},
      %{source: {:url, "https://cdn2.example.com/20mb.br"}, encoding: :br}
    ]
  ]

Keys

KeyTypeDefaultNotes
enabled?booltrueWhen false, Bomb.send/2 is a no‑op.
rotationatom:noneDistribution strategy across a list of bombs for the same level.
levelskeywordMap severity level => [bomb_spec, …].

| bypass_header | string | nil | nil | If set, any request containing that header bypasses bombs. Leave nil (or comment out) to disable the feature. |

Bomb spec

%{
  source: {:file, "/path"} | {:s3, bucket: "…", key: "…", opts: [...] } | {:url, "https://…"},
  encoding: :gzip | :zstd | :br | :deflate | atom(),
  bytes: 1_000_000            # optional – sets Content‑Length without stat
}

If :bytes is absent, ExZipProtect tries to derive size via File.stat/2, HEAD request, or S3 object metadata.


💣 Bring Your Own Bomb (BYOB)

ExZipProtect never creates or bundles payloads. A few popular one‑liners:

# 1GB inflate → 1MB gzip
$ dd if=/dev/zero bs=1G count=1 | gzip -c > 1mb-1gb.gz

# 10GB inflate → 10MB zstd (fast)
$ dd if=/dev/zero bs=1G count=10 | zstd -19 -o 10mb-10gb.zst

# 50GB inflate → 10MB brotli (aggressive)
$ dd if=/dev/zero bs=50G count=1 | brotli -q11 -o 10mb-50gb.br

Danger: Decompressing these files locally may hang or crash your machine. Build them in a throwaway container or server.

Upload the resulting file to your chosen storage, then reference it in config.


🚚 Where to host bombs?

Sourcesource: tupleProsCons
Local file{:file, "/srv/bombs/…"}Fast, no networkConsumes disk on every app node
S3 / GCS / MinIO{:s3, bucket: "…", key: "…"}Offloads storage; cheapRequires ex_aws_s3 dep & network I/O
HTTPS URL (CDN){:url, "https://cdn.example.com/…"}Global edge cachingAdds latency; ensure CORS/ACL OK

📈 Telemetry

[:ex_zip_protect, :bomb, :served]

MeasurementsMetadata
:bytes – size (or :unknown):level, :ip, :source

The application supervisor attaches a default Logger.warning/1 handler—you can detach it and forward to Honeycomb, Datadog, OpenTelemetry, etc.


  • Serving bomb files is a form of denial‑of‑service against the client. Ensure this practice is allowed by your provider & jurisdiction.
  • Provide a bypass header (e.g. X‑ZipBomb: false) if you need certain security scanners to skip bombs.
  • Never serve bombs accidentally in dev/staging—leave enabled?: config_env() == :prod.

🛠️ Advanced topics

  • Streaming upgrades — S3/URL senders are synchronous for now; PRs welcome for fully stream‑chunked variants.
  • Custom rotation — Implement the ExZipProtect.Rotation behaviour and set rotation: MyModule.

🤝 Contributing

Issues and PRs are welcome! Please run mix test and keep the README.md examples in sync.


📜 License

ExZipProtect is released under the MIT License.