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.
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:
- Load the source file from disk
- Transform the file (resize, blur, convert format, etc.)
- Save the transformed result in a cache directory
- 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