Skip to content

Playful way to protect your Phoenix apps from bots and abuse

License

Notifications You must be signed in to change notification settings

houllette/ex_zip_protect

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ExZipProtect

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

Feature Details
BYOB Bring 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‑guard Entire library can be disabled with a single flag (default off in non‑prod envs).
Enable bypass header Configure an optional HTTP header to serve as bypass to ExZipProtect (useful for security scanners).
Telemetry audit event Emits [:ex_zip_protect, :bomb, :served] so you can track activations.
Mix generator mix 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

Key Type Default Notes
enabled? bool true When false, Bomb.send/2 is a no‑op.
rotation atom :none Distribution strategy across a list of bombs for the same level.
levels keyword Map severity level => [bomb_spec, …].
bypass_header string 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?

Source source: tuple Pros Cons
Local file {:file, "/srv/bombs/…"} Fast, no network Consumes disk on every app node
S3 / GCS / MinIO {:s3, bucket: "…", key: "…"} Offloads storage; cheap Requires ex_aws_s3 dep & network I/O
HTTPS URL (CDN) {:url, "https://cdn.example.com/…"} Global edge caching Adds latency; ensure CORS/ACL OK

📈 Telemetry

[:ex_zip_protect, :bomb, :served]

Measurements Metadata
: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.


🛡️ Legal & ethical notes

  • 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.

About

Playful way to protect your Phoenix apps from bots and abuse

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages