GHSA-364Q-W7VH-VHPC
Vulnerability from github – Published: 2026-03-11 00:09 – Updated: 2026-03-11 05:46When the saveLogs feature is enabled, OliveTin persists execution log entries to disk. The filename used for these log files is constructed in part from the user-supplied UniqueTrackingId field in the StartAction API request. This value is not validated or sanitized before being used in a file path, allowing an attacker to use directory traversal sequences (e.g., ../../../) to write files to arbitrary locations on the filesystem.
Affected Code
Entry point — service/internal/api/api.go (line 130):
The UniqueTrackingId from the API request is passed directly to the executor without validation:
execReq := executor.ExecutionRequest{
Binding: pair,
TrackingID: req.Msg.UniqueTrackingId, // user-controlled, no validation
// ...
}
Tracking ID accepted as-is — service/internal/executor/executor.go (lines 508–512):
The tracking ID is only replaced with a UUID if it is empty or a duplicate. Any other string, including one containing path separators, is accepted:
_, isDuplicate := e.GetLog(req.TrackingID)
if isDuplicate || req.TrackingID == "" {
req.TrackingID = uuid.NewString()
}
Filename construction — service/internal/executor/executor.go (line 1042):
The tracking ID is interpolated directly into the log filename:
filename := fmt.Sprintf("%v.%v.%v",
req.logEntry.ActionTitle,
req.logEntry.DatetimeStarted.Unix(),
req.logEntry.ExecutionTrackingID,
)
File write — service/internal/executor/executor.go (lines 1068–1069 and 1082–1083):
The filename is joined to the configured log directory using path.Join, which calls path.Clean internally. path.Clean resolves .. path segments, causing the final file path to escape the intended directory:
// Results file (.yaml)
filepath := path.Join(dir, filename+".yaml")
err = os.WriteFile(filepath, data, 0600)
// Output file (.log)
filepath := path.Join(dir, filename+".log")
err := os.WriteFile(filepath, []byte(data), 0600)
Proof of Concept
An attacker sends the following StartAction request (Connect RPC or REST):
{
"bindingId": "<any-executable-action-id>",
"uniqueTrackingId": "../../../tmp/pwned"
}
Assuming the action title is Ping the Internet and the timestamp is 1741320000, the constructed filename becomes:
Ping the Internet.1741320000.../../../tmp/pwned
When path.Join processes this with a configured results directory like /var/olivetin/logs:
path.Join("/var/olivetin/logs", "Ping the Internet.1741320000.../../../tmp/pwned.yaml")
path.Clean resolves the traversal:
- Path segments:
["var", "olivetin", "logs", "Ping the Internet.1741320000...", "..", "..", "..", "tmp", "pwned.yaml"] - The
..segments traverse upward past the log directory. - Final resolved path:
/tmp/pwned.yaml
Two files are written:
.yamlfile — contains YAML-serializedInternalLogEntry(action title, icon, timestamps, exit code, output, tags, username, tracking ID).logfile — contains the raw command output (potentially attacker-influenced if the action echoes its arguments)
Impact
- Arbitrary file write to any path writable by the OliveTin process.
- OliveTin frequently runs as root inside Docker containers, so the writable scope is often the entire filesystem.
- An attacker could:
- Overwrite OliveTin's own
sessions.yamlto inject authenticated sessions. - Write to entity file directories to inject malicious entity data.
- Write to system cron directories or other locations to achieve remote code execution.
- Cause denial of service by overwriting critical system files.
Suggested Fix
Validate the UniqueTrackingId to ensure it only contains safe characters before use. A strict UUID format check is the simplest approach:
import "regexp"
var validTrackingID = regexp.MustCompile(`^[a-fA-F0-9\-]+$`)
// In ExecRequest, before accepting the user-supplied ID:
if req.TrackingID == "" || !validTrackingID.MatchString(req.TrackingID) {
req.TrackingID = uuid.NewString()
}
Alternatively, sanitize the filename in stepSaveLog by stripping or rejecting path separators and .. sequences.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/OliveTin/OliveTin"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.0.0-20260309102040-b03af0e2eca3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-31817"
],
"database_specific": {
"cwe_ids": [
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-11T00:09:41Z",
"nvd_published_at": "2026-03-10T22:16:19Z",
"severity": "HIGH"
},
"details": "When the `saveLogs` feature is enabled, OliveTin persists execution log entries to disk. The filename used for these log files is constructed in part from the user-supplied `UniqueTrackingId` field in the `StartAction` API request. This value is not validated or sanitized before being used in a file path, allowing an attacker to use directory traversal sequences (e.g., `../../../`) to write files to arbitrary locations on the filesystem.\n### Affected Code\n\n**Entry point \u2014 `service/internal/api/api.go` (line 130):**\n\nThe `UniqueTrackingId` from the API request is passed directly to the executor without validation:\n\n```go\nexecReq := executor.ExecutionRequest{\n Binding: pair,\n TrackingID: req.Msg.UniqueTrackingId, // user-controlled, no validation\n // ...\n}\n```\n\n**Tracking ID accepted as-is \u2014 `service/internal/executor/executor.go` (lines 508\u2013512):**\n\nThe tracking ID is only replaced with a UUID if it is empty or a duplicate. Any other string, including one containing path separators, is accepted:\n\n```go\n_, isDuplicate := e.GetLog(req.TrackingID)\n\nif isDuplicate || req.TrackingID == \"\" {\n req.TrackingID = uuid.NewString()\n}\n```\n\n**Filename construction \u2014 `service/internal/executor/executor.go` (line 1042):**\n\nThe tracking ID is interpolated directly into the log filename:\n\n```go\nfilename := fmt.Sprintf(\"%v.%v.%v\",\n req.logEntry.ActionTitle,\n req.logEntry.DatetimeStarted.Unix(),\n req.logEntry.ExecutionTrackingID,\n)\n```\n\n**File write \u2014 `service/internal/executor/executor.go` (lines 1068\u20131069 and 1082\u20131083):**\n\nThe filename is joined to the configured log directory using `path.Join`, which calls `path.Clean` internally. `path.Clean` resolves `..` path segments, causing the final file path to escape the intended directory:\n\n```go\n// Results file (.yaml)\nfilepath := path.Join(dir, filename+\".yaml\")\nerr = os.WriteFile(filepath, data, 0600)\n\n// Output file (.log)\nfilepath := path.Join(dir, filename+\".log\")\nerr := os.WriteFile(filepath, []byte(data), 0600)\n```\n\n### Proof of Concept\n\nAn attacker sends the following `StartAction` request (Connect RPC or REST):\n\n```json\n{\n \"bindingId\": \"\u003cany-executable-action-id\u003e\",\n \"uniqueTrackingId\": \"../../../tmp/pwned\"\n}\n```\n\nAssuming the action title is `Ping the Internet` and the timestamp is `1741320000`, the constructed filename becomes:\n\n```\nPing the Internet.1741320000.../../../tmp/pwned\n```\n\nWhen `path.Join` processes this with a configured results directory like `/var/olivetin/logs`:\n\n```\npath.Join(\"/var/olivetin/logs\", \"Ping the Internet.1741320000.../../../tmp/pwned.yaml\")\n```\n\n`path.Clean` resolves the traversal:\n\n1. Path segments: `[\"var\", \"olivetin\", \"logs\", \"Ping the Internet.1741320000...\", \"..\", \"..\", \"..\", \"tmp\", \"pwned.yaml\"]`\n2. The `..` segments traverse upward past the log directory.\n3. Final resolved path: `/tmp/pwned.yaml`\n\nTwo files are written:\n\n- **`.yaml` file** \u2014 contains YAML-serialized `InternalLogEntry` (action title, icon, timestamps, exit code, output, tags, username, tracking ID)\n- **`.log` file** \u2014 contains the raw command output (potentially attacker-influenced if the action echoes its arguments)\n\n### Impact\n\n- **Arbitrary file write** to any path writable by the OliveTin process.\n- OliveTin frequently runs as root inside Docker containers, so the writable scope is often the entire filesystem.\n- An attacker could:\n - Overwrite OliveTin\u0027s own `sessions.yaml` to inject authenticated sessions.\n - Write to entity file directories to inject malicious entity data.\n - Write to system cron directories or other locations to achieve remote code execution.\n - Cause denial of service by overwriting critical system files.\n\n### Suggested Fix\n\nValidate the `UniqueTrackingId` to ensure it only contains safe characters before use. A strict UUID format check is the simplest approach:\n\n```go\nimport \"regexp\"\n\nvar validTrackingID = regexp.MustCompile(`^[a-fA-F0-9\\-]+$`)\n\n// In ExecRequest, before accepting the user-supplied ID:\nif req.TrackingID == \"\" || !validTrackingID.MatchString(req.TrackingID) {\n req.TrackingID = uuid.NewString()\n}\n```\n\nAlternatively, sanitize the filename in `stepSaveLog` by stripping or rejecting path separators and `..` sequences.",
"id": "GHSA-364q-w7vh-vhpc",
"modified": "2026-03-11T05:46:08Z",
"published": "2026-03-11T00:09:41Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/OliveTin/OliveTin/security/advisories/GHSA-364q-w7vh-vhpc"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-31817"
},
{
"type": "WEB",
"url": "https://github.com/OliveTin/OliveTin/commit/2f77000de44f65690f257e3cf8e2c8462b0e74c7"
},
{
"type": "PACKAGE",
"url": "https://github.com/OliveTin/OliveTin"
},
{
"type": "WEB",
"url": "https://github.com/OliveTin/OliveTin/releases/tag/3000.11.2"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:L",
"type": "CVSS_V3"
}
],
"summary": "OliveTin\u0027s unsafe parsing of UniqueTrackingId can be used to write files"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.