Résumé
ageLANServer: Unbounded JSON Array Allocation in AoE3 Cloud `getFileURL` Endpoint Leads to Remote Denial of Service
Détails de l’avis
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:
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-36only checks thatsessionIDresolves to an existing session — it performs no additional authorization.- The default configuration
server/resources/config/config.tomlsetsAuthentication = 'disabled'. Combined withserver/internal/routes/game/login/platformlogin.go, which issues a session for any client-suppliedaccountType/platformUserIDwithout verifying it against a real platform, an attacker can obtain a validsessionIDwith a single unauthenticatedplatformloginrequest and no prior credentials.
Data flow (source → sink):
server/resources/unix/start_age3.sh:4(orstart_age3.bat) starts the release server with-e age3, the default supported way to run the AoE3 server.server/internal/routes/router/game.go:267registersPOST /game/cloud/getFileURL.server/internal/routes/router/sessionMiddleware.go:23-36accepts any request bearing a validsessionID.server/internal/http.godecodes the JSON body intogetFileURLRequestwith no size limiting.server/internal/routes/game/cloud/getFileURL.go:13-19binds the attacker-controllednamesarray intoreq.Names.server/internal/routes/game/cloud/getFileURL.go:27and:30allocate slices sized directly bylen(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:
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:
- Calls
POST /game/login/platformloginwith an arbitrary Steam-styleaccountType/platformUserID/macAddressto obtain a session id, exploiting the defaultAuthentication = 'disabled'setting. - Sends a small baseline
POST /game/cloud/getFileURL?sessionID=<id>request (namesarray with 5 elements) and records server RSS before/after viadocker exec ... cat /proc/1/status. - Sends a single request with
namescontaining 2,000,000 elements and records the resulting RSS growth relative to request size (memory amplification). - Against the same container restarted with
--memory=300m, fires 3 concurrent requests each withnamescontaining 3,000,000 elements, then inspectsdocker inspectstate to check for an OOM kill.
python3 poc.py --host 127.0.0.1 --port 8443 --container agelan-vuln001-test
Observed results (Phase 2 dynamic reproduction, unmodified source):
- Baseline request (
namescount = 5, 43 request bytes): server RSS unchanged (16928 KB → 16928 KB). - Single moderately large request (
namescount = 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 a11.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 ofnamescount = 3,000,000 each (18,000,013 bytes each) were sent; all 3 connections failed withRemoteDisconnected: Remote end closed connection without response.docker inspectconfirmed the container transitioned fromstatus=running, OOMKilled=falsetostatus=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
# 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
Références
- https://github.com/advisories/GHSA-4jfq-pmq9-257h
- https://github.com/luskaner/ageLANServer/security/advisories/GHSA-4jfq-pmq9-257h
- https://nvd.nist.gov/vuln/detail/CVE-2026-107839
- https://github.com/luskaner/ageLANServer/commit/1dd40166a59356865c0a4b1800f32d05dc8bec79
- https://github.com/luskaner/ageLANServer/releases/tag/v1.15.2
Vulnérabilités liées
Tout Supply chain →- MEDIUMCVE-2026-95838
containerd: Unpack DoS via repeated opaque whiteouts
- MEDIUMCVE-2026-100350
kin-openapi deepObject query decoding repeatedly compiles an invariant regular expression
- MEDIUMCVE-2026-59951
MCP Python SDK: Unbounded Session Accumulation in StreamableHTTP Transport Enables Resource Exhaustion
- MEDIUMCVE-2026-104150
AdonisJS HTTP Server: Uncontrolled Resource Consumption and Inefficient Regular Expression Complexity
- HIGHCVE-2026-72923
Microsoft.OpenApi.YamlReader vulnerable to denial of service via YAML alias expansion
- HIGHCVE-2026-107805
Nginx UI: Unauthenticated signed-request body staging can exhaust temporary storage