ageLANServer: Unbounded JSON Array Allocation in AoE3 Cloud `getFileURL` Endpoint Leads to Remote Denial of Service
CVE-2026-107839 · GHSA-4jfq-pmq9-257h · GO-2026-6716 · published 9 Oct 2026
How serious is it
- Severity: high, CVSS score 7.5 out of 10.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-107839?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
github.com/luskaner/ageLANServer/server(Go) < 1.8.2-0.20260809203301-1dd40166a593
How to check
- Run
go list -m github.com/luskaner/ageLANServer/serverin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- Upgrade
github.com/luskaner/ageLANServer/serverto 1.8.2-0.20260809203301-1dd40166a593 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
### Summary
The AoE3 `POST /game/cloud/getFileURL` handler in `luskaner/ageLANServer`'s bundled game server decodes an attacker-controlled `names` JSON array and immediately allocates response storage sized directly from the array's length (`make(i.A, len(req.Names.Data))`), with no request body size limit and no cap on the number of array elements anywhere in the request path. Because the default configuration ships with `Authentication = 'disabled'`, any network client can obtain a session through unauthenticated platform login and then send a single crafted request that forces the server to allocate memory proportional to attacker-chosen input. Dynamic testing against an unmodified build confirmed both large memory-amplification growth from a single request and a full server process kill via the kernel OOM killer when a memory-constrained instance received a few concurrent oversized requests. This is a high-severity, pre-authentication (self-registration only) remote denial-of-service vulnerability (CVSS 3.1 Base Score 7.5, CWE-400).
### Details
`server/internal/routes/game/cloud/getFileURL.go` defines the request struct and handler for the AoE3 cloud "get file URL" endpoint:
```go
type getFileURLRequest struct {
Names i.Json[[]string] `json:"names"`
}
func GetFileURL(w http.ResponseWriter, r *http.Request) {
var req getFileURLRequest
err := i.Bind(r, &req)
...
descriptions := make(i.A, len(req.Names.Data))
for j, name := range req.Names.Data {
...
```
`Names` is typed as `i.Json[[]string]`, a wrapper whose `UnmarshalText` is invoked by `encoding/json` whenever the incoming JSON value for `names` is itself a string, so the attacker supplies a JSON-string-encoded array (e.g. `{"names":"[\"x\",\"x\",...]"}`). The request body is decoded in `server/internal/http.go`'s `Bind()` via `json.NewDecoder(r.Body).Decode(data)`, with no `http.MaxBytesReader`, no `LimitReader`, and no `MaxHeaderBytes` configured on the `http.Server` in `server/internal/cmd/root.go`. There is also no branch-neutral cap on `len(req.Names.Data)` — the same unbounded length is also used at `getFileURL.go:27` (`slices.Repeat(i.A{nil}, len(req.Names.Data))` when the cloud file cache is empty) in addition to the `make(i.A, len(req.Names.Data))` sink at line 30.
Because `len()` on the attacker-supplied slice is used directly as an allocation size with no upper bound, an attacker can force the server to allocate memory (and CPU cycles decoding/iterating) proportional to the size of a single HTTP request body, which is itself unbounded.
Reaching this code path requires only a valid session, not real game ownership or credentials:
- The endpoint is registered for the AoE3 title at `server/internal/routes/router/game.go:267`.
- `server/internal/routes/router/sessionMiddleware.go:23-36` only checks that `sessionID` resolves to an existing session — it performs no additional authorization.
- The default configuration `server/resources/config/config.toml` sets `Authentication = 'disabled'`. Combined with `server/internal/routes/game/login/platformlogin.go`, which issues a session for any client-supplied `accountType`/`platformUserID` without verifying it against a real platform, an attacker can obtain a valid `sessionID` with a single unauthenticated `platformlogin` request and no prior credentials.
Data flow (source → sink):
1. `server/resources/unix/start_age3.sh:4` (or `start_age3.bat`) starts the release server with `-e age3`, the default supported way to run the AoE3 server.
2. `server/internal/routes/router/game.go:267` registers `POST /game/cloud/getFileURL`.
3. `server/internal/routes/router/sessionMiddleware.go:23-36` accepts any request bearing a valid `sessionID`.
4. `server/internal/http.go` decodes the JSON body into `getFileURLRequest` with no size limiting.
5. `server/internal/routes/game/cloud/getFileURL.go:13-19` binds the attacker-controlled `names` array into `req.Names`.
6. `server/internal/routes/game/cloud/getFileURL.go:27` and `:30` allocate slices sized directly by `len(req.Names.Data)`.
### PoC
Environment: Docker (see `Dockerfile`), which builds the **unmodified** `server` and `genCert` binaries directly from the repository source at commit `405b9a9` (equivalent to the previously reported `870b90c`) and starts the AoE3 server (`./server -e age3`) with the shipped default configuration (`Authentication = 'disabled'`), only disabling LAN announcement broadcast since it is irrelevant to the sandboxed reproduction.
Build and run:
```bash
docker build -f vuln-001/Dockerfile -t agelan-vuln001 repo
docker run -d --name agelan-vuln001-test -p 8443:443 --memory=300m agelan-vuln001
```
Attack script: `poc.py` (see file for full source). At a high level it:
1. Calls `POST /game/login/platformlogin` with an arbitrary Steam-style `accountType`/`platformUserID`/`macAddress` to obtain a session id, exploiting the default `Authentication = 'disabled'` setting.
2. Sends a small baseline `POST /game/cloud/getFileURL?sessionID=<id>` request (`names` array with 5 elements) and records server RSS before/after via `docker exec ... cat /proc/1/status`.
3. Sends a single request with `names` containing 2,000,000 elements and records the resulting RSS growth relative to request size (memory amplification).
4. Against the same container restarted with `--memory=300m`, fires 3 concurrent requests each with `names` containing 3,000,000 elements, then inspects `docker inspect` state to check for an OOM kill.
```bash
python3 poc.py --host 127.0.0.1 --port 8443 --container agelan-vuln001-test
```
Observed results (Phase 2 dynamic reproduction, unmodified source):
- Baseline request (`names` count = 5, 43 request bytes): server RSS unchanged (16928 KB → 16928 KB).
- Single moderately large request (`names` count = 2,000,000, 12,000,013 request bytes): server RSS jumped from 16,984 KB to 224,724 KB — a growth of 207,740 KB from a ~11.4 MB request (~17.73x amplification of request size in RSS growth).
- Concurrent att…
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/luskaner/ageLANServer/security/advisories/GHSA-4jfq-pmq9-257h
- nvd.nist.gov/vuln/detail/CVE-2026-107839
- github.com/luskaner/ageLANServer/commit/1dd40166a59356865c0a4b1800f32d05dc8bec79
- github.com/luskaner/ageLANServer/releases/tag/v1.15.2
- github.com/advisories/GHSA-4jfq-pmq9-257h
- github.com/luskaner/ageLANServer
Merged from OSV, GitHub Advisory Database, NVD. Last updated 24 Oct 2026. All vulnerabilities · RSS · JSON