---
id: "CVE-2026-107839"
kind: "vulnerability"
aliases: ["CVE-2026-107839", "GHSA-4jfq-pmq9-257h", "GO-2026-6716"]
title: "ageLANServer: Unbounded JSON Array Allocation in AoE3 Cloud `getFileURL` Endpoint Leads to Remote Denial of Service"
severity: "high"
cvss: 7.5
epss: 0.0034199999645352364
exploited: false
status: "open"
category: "package"
products: [{"name": "github.com/luskaner/ageLANServer/server", "fixed": ["1.8.2-0.20260809203301-1dd40166a593"], "affected": "< 1.8.2-0.20260809203301-1dd40166a593", "ecosystem": "go"}]
ecosystems: ["go"]
tags: ["age-of-empires-iii", "cwe-400", "go", "lan-server", "package", "self-hosted"]
not_affected_if: ["managed-hosting", "not-running-age3-server", "pinned-fixed-version"]
published_at: "2026-10-09T18:17:05+00:00"
first_seen_at: "2026-10-23T19:11:32+00:00"
updated_at: "2026-10-23T23:44:52+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-4jfq-pmq9-257h", "first_seen_at": "2026-10-23T19:11:32+00:00"}, {"source": "govuln", "url": "https://pkg.go.dev/vuln/GO-2026-6716", "first_seen_at": "2026-10-23T19:13:08+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-107839", "first_seen_at": "2026-10-23T23:44:52+00:00"}]
---

# ageLANServer: Unbounded JSON Array Allocation in AoE3 Cloud `getFileURL` Endpoint Leads to Remote Denial of Service

ageLANServer's AoE3 getFileURL endpoint allocates memory sized directly from an attacker-supplied JSON array length with no body or element limit. An unauthenticated client can get a session through the default login flow and crash the server with a single large request or a few concurrent ones.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- go:github.com/luskaner/ageLANServer/server affected < 1.8.2-0.20260809203301-1dd40166a593 fixed 1.8.2-0.20260809203301-1dd40166a593

## How to check

Check whether the server module github.com/luskaner/ageLANServer/server is below 1.8.2-0.20260809203301-1dd40166a593 (go list -m or go.sum).

## Fix

Upgrade github.com/luskaner/ageLANServer/server to 1.8.2-0.20260809203301-1dd40166a593 or later.
If upgrading is not possible, set Authentication to a value other than 'disabled' and restrict network access to the game server port (unverified mitigation, sources do not confirm it blocks the attack).

## Timeline

- 2026-10-23T19:11:32+00:00 ghsa https://github.com/advisories/GHSA-4jfq-pmq9-257h
- 2026-10-23T19:13:08+00:00 govuln https://pkg.go.dev/vuln/GO-2026-6716
- 2026-10-23T23:44:52+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-107839

## From the sources

### 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 attack phase against the same container limited to `--memory=300m`: 3 concurrent requests of `names` count = 3,000,000 each (18,000,013 bytes each) were sent; all 3 connections failed with `RemoteDisconnected: Remote end closed connection without response`. `docker inspect` confirmed the container transitioned from `status=running, OOMKilled=false` to `status=exited, OOMKilled=true, ExitCode=137` — the kernel OOM killer terminated the server process.

No source code in the target repository was modified for this reproduction; the container builds and runs the vulnerable code exactly as shipped.

### Impact
This is a remote, network-reachable, pre-authentication (self-registration-only) denial-of-service vulnerability (CWE-400: Uncontrolled Resource Consumption). Any client able to reach the AoE3 game server's HTTPS listener — the default and documented way to run `ageLANServer` for Age of Empires III — can obtain a session via the default unauthenticated login flow and then crash or hang the server process with a single well-crafted HTTP request or a small number of concurrent requests, without needing legitimate game credentials, prior interaction, or user interaction. Impact is availability-only (no confidentiality or integrity impact observed): all players connected to the affected LAN server lose service until the operator restarts the process.

### Reproduction artifacts

#### `Dockerfile`

```dockerfile
# syntax=docker/dockerfile:1
#
# VULN-001 PoC image for luskaner/ageLANServer.
# Builds the *unmodified* server and genCert binaries straight from the cloned
# repository source (no source-code edits) and runs the AoE3 game server so the
# unbounded-allocation "names" array bug in
#   server/internal/routes/game/cloud/getFileURL.go
# can be triggered over the network exactly as an attacker would.
#
# Build context MUST be the ageLANServer repository root, e.g.:
#   docker build -f vuln-001/Dockerfile -t agelan-vuln001 <path-to-repo>

FROM golang:1.26-alpine3.24 AS

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
