GitHub Security Advisory: GHSA-364q-w7vh-vhpc
Package: github.com/OliveTin/OliveTin (GO)
Severity: HIGH (CVSS 8.5)
CWE: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Vulnerable versions: < 0.0.0[PII_REDACTED]2040-b03af0e2eca3
Patched in: 0.0.0[PII_REDACTED]2040-b03af0e2eca3

Summary: OliveTin's unsafe parsing of UniqueTrackingId can be used to write files

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.
### 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:

```go
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:

```go
_, 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:

```go
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:

```go
// 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):

```json
{
  "bindingId": "<any-executable-action-id>",
  "uniqueTrackingId": "../../../tmp/pwned"
}
```

Assuming the action title is `Ping the Internet` and the timestamp is `[PII_REDACTED]`, the constructed filename becomes:

```
Ping the Internet[PII_REDACTED].../../../tmp/pwned
```

When `path.Join` processes this with a configured results directory like `/var/olivetin/logs`:

```
path.Join("/var/olivetin/logs", "Ping the Internet[PII_REDACTED].../../../tmp/pwned.yaml")
```

`path.Clean` resolves the traversal:

1. Path segments: `["var", "olivetin", "logs", "Ping the Internet[PII_REDACTED]...", "..", "..", "..", "tmp", "pwned.yaml"]`
2. The `..` segments traverse upward past the log directory.
3. Final resolved path: `/tmp/pwned.yaml`

Two files are written:

- **`.yaml` file** — contains YAML-serialized `InternalLogEntry` (action title, icon, timestamps, exit code, output, tags, userna