← Back to list

Security Audit & Architecture Review: Hardening a Go Universal Proxy‑CDN

Building infrastructure software often begins with a very pragmatic idea: make something fast that solves a real problem.

l3dlp · 2026-03-11 02:04 · 0 claps · 5.0 min read
#golang-development #gin-gonic #cdn-service #image-cdn #open-source-software
Open on Medium ↗
Wiki topics: RAG · RAG & Retrieval 🔒 · Cybersecurity 🔓 · Open Source 🏛️ · Architecture

Security Audit & Architecture Review: Hardening a Go Universal Proxy‑CDN

Building infrastructure software often begins with a very pragmatic idea: make something fast that solves a real problem.

In this case, the goal was simple but powerful — transform a folder of static assets into a dynamic API capable of:

  • resizing images on the fly
  • converting images to WebP
  • exporting XLSX sheets as CSV or JSON

All while maintaining the performance characteristics expected from a CDN‑style system.

The implementation was written in Go using the Gin web framework. The design emphasized performance and minimal allocations, relying heavily on caching strategies and the operating system’s capabilities.

At first glance, everything worked beautifully. Requests returned in under a millisecond when cached. Disk reads were minimized. HTTP validators (ETag and Last‑Modified) behaved correctly.

But there is a massive difference between “code that works” and “code that survives the internet”.

This article explains how a seemingly robust Go service was audited, what vulnerabilities were discovered, and how they were fixed. The goal is to make these concepts accessible to developers who are comfortable with Go but may be new to backend security considerations.

The Initial Architecture

The service acts as a transformer sitting in front of a directory of static files.

Example request:

/o/resize/photos/cat.jpg?800x600

The server performs the following operations:

  1. Load the source file from disk
  2. Transform the file (resize, blur, convert format, etc.)
  3. Save the transformed result in a cache directory
  4. Serve cached results for future requests

This architecture avoids recomputation and scales very well.

Two cache layers were implemented.

Source Image Cache (LRU)

Decoded source images are stored in an in‑memory LRU cache. Decoding an image is expensive, so caching the decoded representation avoids repeating this work.

Memory‑Mapped Cache

Generated files are stored on disk, but frequently requested files are served through memory‑mapped IO (mmap).

Memory mapping allows the OS to manage file caching and eliminates repeated read system calls.

In practice this means:

  • very low latency
  • minimal CPU usage
  • minimal disk overhead

At this stage the system was extremely efficient.

But efficiency does not equal safety.

Why Security Reviews Matter

Backend services are exposed to an adversarial environment.

Users do not always send “reasonable” requests. Bots, scanners, and attackers deliberately probe systems using malformed inputs designed to trigger edge cases.

Common attack categories include:

  • path traversal
  • resource exhaustion (CPU, memory, disk)
  • decompression bombs
  • request amplification

Many of these attacks do not rely on sophisticated exploits. They rely on unexpected input sizes.

The audit focused on identifying situations where user input could cause the server to consume disproportionate resources.

Vulnerability 1 — Path Traversal

The Problem

The server accepts a path parameter which determines the file to process.

A simplified version of the code looked like this:

relPath := filepath.Clean(strings.TrimPrefix(rawPath, "/"))
sourceFile := filepath.Join(SourceDir, relPath)

At first glance this looks safe.

filepath.Clean() removes obvious "../" segments, so it appears to prevent traversal attacks.

However, this assumption is fragile.

Different layers of infrastructure may decode or normalize paths differently:

  • reverse proxies
  • load balancers
  • URL encoding

An attacker might send:

..%2f..%2fetc/passwd

Depending on how decoding occurs upstream, the application might eventually process it as a traversal path.

The Correct Strategy

Input validation alone is not sufficient.

You must validate the final resolved path.

Example fix:

fullPath := filepath.Join(SourceDir, relPath)
if !strings.HasPrefix(fullPath, SourceDir+string(os.PathSeparator)) {
    return error
}

This ensures the resulting path is still inside the expected directory.

A more robust approach is using filepath.Rel:

rel, err := filepath.Rel(SourceDir, fullPath)
if err != nil || strings.HasPrefix(rel, "..") {
    return error
}

This eliminates the possibility of escaping the root directory.

Vulnerability 2 — Image Decompression Bomb (Memory DoS)

The Problem

Images are decoded using:

imaging.Open(path)

The issue is that decoding an image allocates memory proportional to the image’s pixel count.

An attacker can upload or reference an image whose header declares enormous dimensions.

Example:

100000 x 100000 pixels

Even if the file itself is only a few bytes, the decoder attempts to allocate a buffer for the full image.

This can easily consume multiple gigabytes of RAM and crash the process.

The Fix

Read the image header before decoding the full image.

cfg, _, err := image.DecodeConfig(file)

This returns only metadata.

If the dimensions exceed a safe threshold, the request is rejected before allocating memory.

Example safeguard:

const maxImageDimension = 8000
if cfg.Width > maxImageDimension || cfg.Height > maxImageDimension {
    return error
}

This simple check prevents memory exhaustion attacks.

Vulnerability 3 — CPU Exhaustion via Resize Parameters

The Problem

The API accepts resize dimensions through the query string:

?800x600

However, nothing prevented a request like:

?99999x99999

Image resizing algorithms (especially Lanczos) are computationally expensive.

Their cost roughly scales with the number of pixels produced.

If a malicious client requests extremely large dimensions repeatedly, the CPU can be saturated.

The Fix

Clamp requested dimensions.

const maxResize = 4000
if w > maxResize {
    w = maxResize
}
if h > maxResize {
    h = maxResize
}

This preserves functionality while preventing runaway CPU usage.

Vulnerability 4 — Disk Exhaustion via Cache Variants

The Problem

Each resize request generates a cached file on disk.

For example:

/cache/resize/800x600/image.jpg

If an attacker continuously requests new dimension combinations:

1x1
2x2
3x3
...

The server will generate thousands of cached files.

Eventually the disk fills up.

The Fix

Limit the number of cache variants per source image.

This can be done by counting existing variants in the cache directory.

If the limit is exceeded, the server rejects additional variants.

Example strategy:

maxVariantsPerImage := 20

This prevents disk flooding attacks while still allowing useful caching.

Vulnerability 5 — XLSX Zip Bombs

The Problem

An XLSX file is actually a ZIP archive containing XML files.

A “zip bomb” is a compressed file that expands into enormous data once decompressed.

A tiny XLSX file could expand to hundreds of megabytes of XML content.

If the server blindly decompresses it, memory usage can spike dramatically.

The Fix

Limit the decompression size when opening XLSX files.

Many libraries allow configuration for maximum uncompressed size.

Example conceptually:

UnzipXMLSizeLimit = 250MB

Requests exceeding this limit are rejected.

Vulnerability 6 — Rate Limiting and Proxy Awareness

The Problem

The rate limiter relies on the client’s IP address:

c.ClientIP()

However, when a service is deployed behind a reverse proxy (such as Nginx or Caddy), the actual client IP may be transmitted via headers like:

X‑Forwarded‑For

If the application trusts these headers blindly, attackers can spoof their IP address.

This bypasses the rate limiter.

The Fix

Explicitly define trusted proxies.

Only accept forwarded headers when the request originates from known proxy addresses.

Example configuration:

router.SetTrustedProxies([]string{"127.0.0.1"})

This ensures IP detection remains reliable.

Lessons Learned

Security vulnerabilities rarely appear in normal usage.

They appear when systems are pushed far beyond their expected input range.

In practice, most production hardening involves three principles:

Always Validate Final State

Do not only validate inputs. Validate the resolved result (paths, sizes, memory allocations).

Impose Resource Limits

Limit:

  • maximum image size
  • maximum resize dimensions
  • maximum cache variants
  • maximum decompression size

Assume Adversarial Traffic

Any public API will eventually receive malicious requests.

Design the system so that worst‑case inputs remain cheap to reject.

Final Thoughts

The difference between a prototype and production‑grade infrastructure is often just a handful of defensive checks.

A few constants and validation rules can prevent entire classes of attacks.

The core lesson is simple:

Every input is hostile until proven otherwise.

Once this mindset becomes habitual, writing secure backend services becomes far easier.

And the internet becomes a much less dangerous place for your code.

Btw, the GitHub of my project: https://github.com/l3dlp/transmuter

I welcome all feedback :)


메타데이터
post_id
eae4adea94d9
slug
security-audit-architecture-review-hardening-a-go-universal-proxy-cdn-eae4adea94d9
url
https://medium.com/@l3dlp/security-audit-architecture-review-hardening-a-go-universal-proxy-cdn-eae4adea94d9
canonical_url
https://medium.com/@l3dlp/security-audit-architecture-review-hardening-a-go-universal-proxy-cdn-eae4adea94d9
author_url
https://medium.com/@l3dlp
status
ok
fetched_at
2026-07-13 06:23:13