GHSA-JM28-2WCR-QF3H
Vulnerability from github – Published: 2026-07-30 14:25 – Updated: 2026-07-30 14:25Summary
The synchronous execution RPCs StartActionAndWait and StartActionByGetAndWait return the full LogEntry for the just-executed action without checking whether the caller is allowed to read that action's logs.
OliveTin's ACL model separates exec from logs. A deployment can intentionally allow a user to run an action while denying access to its historical or live output. That separation is enforced in GetLogs, GetActionLogs, ExecutionStatus, and EventStream, but it is not enforced in the synchronous ...AndWait endpoints.
As a result, any user who can execute an action through these endpoints can read the action output immediately even when the action's ACL explicitly sets logs:false.
Details
OliveTin defines separate per-action permissions:
// service/internal/config/config.go
type PermissionsList struct {
View bool `koanf:"view"`
Exec bool `koanf:"exec"`
Logs bool `koanf:"logs"`
Kill bool `koanf:"kill"`
}
The normal log and streaming paths correctly enforce logs permission:
// service/internal/api/api.go
func (api *oliveTinAPI) isLogEntryAllowed(e *executor.InternalLogEntry, user *authpublic.AuthenticatedUser) bool {
if user == nil || !isValidLogEntry(e) {
return false
}
return acl.IsAllowedLogs(api.cfg, user, e.Binding.Action)
}
That check is used by:
GetLogsGetActionLogsExecutionStatusEventStream
However, the synchronous execution endpoints directly return the created LogEntry without any logs ACL check:
// service/internal/api/api.go
func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionAndWaitRequest]) (*connect.Response[apiv1.StartActionAndWaitResponse], error) {
...
internalLogEntry, ok := api.startActionAndWaitRun(binding, args, user)
if !ok {
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found"))
}
return connect.NewResponse(&apiv1.StartActionAndWaitResponse{
LogEntry: api.internalLogEntryToPb(internalLogEntry, user),
}), nil
}
func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetAndWaitRequest]) (*connect.Response[apiv1.StartActionByGetAndWaitResponse], error) {
...
internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
if ok {
return connect.NewResponse(&apiv1.StartActionByGetAndWaitResponse{
LogEntry: api.internalLogEntryToPb(internalLogEntry, user),
}), nil
}
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found"))
}
And internalLogEntryToPb() includes the full output:
// service/internal/api/api.go
func (api *oliveTinAPI) internalLogEntryToPb(logEntry *executor.InternalLogEntry, authenticatedUser *authpublic.AuthenticatedUser) *apiv1.LogEntry {
pble := &apiv1.LogEntry{
ActionTitle: logEntry.ActionTitle,
Output: logEntry.Output,
ExitCode: logEntry.ExitCode,
ExecutionTrackingId: logEntry.ExecutionTrackingID,
...
}
...
return pble
}
The executor separately enforces exec permission, but there is no subsequent check that the response should omit or deny Output when logs:false.
Local Reproduction
I verified this locally with a one-off test against the real StartActionAndWait handler.
Configuration used:
- action
secret_actionwith shellecho SECRET_FROM_ACTION - user
lowmatched by ACL: exec: truelogs: falseview: falsekill: false
Then I invoked StartActionAndWait as low through the real handler path using header-based auth.
Observed output from the test run:
=== RUN TestTempStartActionAndWaitLeaksOutputWithoutLogsPermission
time="2026-03-12T23:36:33+01:00" level=info msg="Action requested" actionTitle=secret-action tags="[]"
time="2026-03-12T23:36:33+01:00" level=info msg="Action parse args - Before" actionTitle=secret-action cmd="echo SECRET_FROM_ACTION"
time="2026-03-12T23:36:33+01:00" level=info msg="Action parse args - After" actionTitle=secret-action cmd="echo SECRET_FROM_ACTION"
time="2026-03-12T23:36:33+01:00" level=info msg="Action started" actionTitle=secret-action timeout=5
time="2026-03-12T23:36:33+01:00" level=info msg="Action finished" actionTitle=secret-action exit=0 outputLength=40 timedOut=false
temp_acl_bypass_test.go:56: output="S\x00E\x00C\x00R\x00E\x00T\x00_\x00F\x00R\x00O\x00M\x00_\x00A\x00C\x00T\x00I\x00O\x00N\x00\r\x00\n\x00" blocked=false action="secret-action"
--- PASS: TestTempStartActionAndWaitLeaksOutputWithoutLogsPermission (0.03s)
The UTF-16LE formatting is from Windows echo, but the key result is that the response returned the real command output even though the caller's ACL explicitly denied log access.
Impact
- Bypass of log confidentiality controls: Operators may configure actions to be executable but not log-readable. The
...AndWaitendpoints break that separation. - Disclosure of secrets printed by actions: Many OliveTin actions wrap administrative scripts and commands whose stdout/stderr may contain credentials, internal paths, hostnames, tokens, or sensitive operational data.
- Adjacent to previous output-leak bugs: OliveTin already had an EventStream output disclosure bug. This is a separate response-path authorization gap on the synchronous execution APIs.
Recommended Fix
Enforce logs permission before returning LogEntry content from synchronous execution endpoints.
Two reasonable fixes:
- Deny the endpoint response when
logs:false
if !acl.IsAllowedLogs(api.cfg, user, binding.Action) {
return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied to view action output"))
}
- Or redact log-bearing fields when
logs:false
pb := api.internalLogEntryToPb(internalLogEntry, user)
if !acl.IsAllowedLogs(api.cfg, user, binding.Action) {
pb.Output = ""
pb.ExitCode = 0
}
return connect.NewResponse(&apiv1.StartActionAndWaitResponse{LogEntry: pb}), nil
The same fix should be applied to both:
StartActionAndWaitStartActionByGetAndWait
References
service/internal/api/api.goservice/internal/config/config.goservice/internal/acl/acl.go
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/OliveTin/OliveTin"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.0.0-20260708085316-e421780c9885"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-67439"
],
"database_specific": {
"cwe_ids": [
"CWE-863"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-30T14:25:20Z",
"nvd_published_at": "2026-07-29T21:17:48Z",
"severity": "MODERATE"
},
"details": "## Summary\n\nThe synchronous execution RPCs `StartActionAndWait` and `StartActionByGetAndWait` return the full `LogEntry` for the just-executed action without checking whether the caller is allowed to read that action\u0027s logs.\n\nOliveTin\u0027s ACL model separates `exec` from `logs`. A deployment can intentionally allow a user to run an action while denying access to its historical or live output. That separation is enforced in `GetLogs`, `GetActionLogs`, `ExecutionStatus`, and `EventStream`, but it is not enforced in the synchronous `...AndWait` endpoints.\n\nAs a result, any user who can execute an action through these endpoints can read the action output immediately even when the action\u0027s ACL explicitly sets `logs:false`.\n\n## Details\n\nOliveTin defines separate per-action permissions:\n\n```go\n// service/internal/config/config.go\ntype PermissionsList struct {\n View bool `koanf:\"view\"`\n Exec bool `koanf:\"exec\"`\n Logs bool `koanf:\"logs\"`\n Kill bool `koanf:\"kill\"`\n}\n```\n\nThe normal log and streaming paths correctly enforce `logs` permission:\n\n```go\n// service/internal/api/api.go\nfunc (api *oliveTinAPI) isLogEntryAllowed(e *executor.InternalLogEntry, user *authpublic.AuthenticatedUser) bool {\n if user == nil || !isValidLogEntry(e) {\n return false\n }\n return acl.IsAllowedLogs(api.cfg, user, e.Binding.Action)\n}\n```\n\nThat check is used by:\n\n- `GetLogs`\n- `GetActionLogs`\n- `ExecutionStatus`\n- `EventStream`\n\nHowever, the synchronous execution endpoints directly return the created `LogEntry` without any `logs` ACL check:\n\n```go\n// service/internal/api/api.go\nfunc (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionAndWaitRequest]) (*connect.Response[apiv1.StartActionAndWaitResponse], error) {\n ...\n internalLogEntry, ok := api.startActionAndWaitRun(binding, args, user)\n if !ok {\n return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf(\"execution not found\"))\n }\n return connect.NewResponse(\u0026apiv1.StartActionAndWaitResponse{\n LogEntry: api.internalLogEntryToPb(internalLogEntry, user),\n }), nil\n}\n\nfunc (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetAndWaitRequest]) (*connect.Response[apiv1.StartActionByGetAndWaitResponse], error) {\n ...\n internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)\n if ok {\n return connect.NewResponse(\u0026apiv1.StartActionByGetAndWaitResponse{\n LogEntry: api.internalLogEntryToPb(internalLogEntry, user),\n }), nil\n }\n return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf(\"execution not found\"))\n}\n```\n\nAnd `internalLogEntryToPb()` includes the full output:\n\n```go\n// service/internal/api/api.go\nfunc (api *oliveTinAPI) internalLogEntryToPb(logEntry *executor.InternalLogEntry, authenticatedUser *authpublic.AuthenticatedUser) *apiv1.LogEntry {\n pble := \u0026apiv1.LogEntry{\n ActionTitle: logEntry.ActionTitle,\n Output: logEntry.Output,\n ExitCode: logEntry.ExitCode,\n ExecutionTrackingId: logEntry.ExecutionTrackingID,\n ...\n }\n ...\n return pble\n}\n```\n\nThe executor separately enforces `exec` permission, but there is no subsequent check that the response should omit or deny `Output` when `logs:false`.\n\n## Local Reproduction\n\nI verified this locally with a one-off test against the real `StartActionAndWait` handler.\n\nConfiguration used:\n\n- action `secret_action` with shell `echo SECRET_FROM_ACTION`\n- user `low` matched by ACL:\n - `exec: true`\n - `logs: false`\n - `view: false`\n - `kill: false`\n\nThen I invoked `StartActionAndWait` as `low` through the real handler path using header-based auth.\n\nObserved output from the test run:\n\n```text\n=== RUN TestTempStartActionAndWaitLeaksOutputWithoutLogsPermission\ntime=\"2026-03-12T23:36:33+01:00\" level=info msg=\"Action requested\" actionTitle=secret-action tags=\"[]\"\ntime=\"2026-03-12T23:36:33+01:00\" level=info msg=\"Action parse args - Before\" actionTitle=secret-action cmd=\"echo SECRET_FROM_ACTION\"\ntime=\"2026-03-12T23:36:33+01:00\" level=info msg=\"Action parse args - After\" actionTitle=secret-action cmd=\"echo SECRET_FROM_ACTION\"\ntime=\"2026-03-12T23:36:33+01:00\" level=info msg=\"Action started\" actionTitle=secret-action timeout=5\ntime=\"2026-03-12T23:36:33+01:00\" level=info msg=\"Action finished\" actionTitle=secret-action exit=0 outputLength=40 timedOut=false\n temp_acl_bypass_test.go:56: output=\"S\\x00E\\x00C\\x00R\\x00E\\x00T\\x00_\\x00F\\x00R\\x00O\\x00M\\x00_\\x00A\\x00C\\x00T\\x00I\\x00O\\x00N\\x00\\r\\x00\\n\\x00\" blocked=false action=\"secret-action\"\n--- PASS: TestTempStartActionAndWaitLeaksOutputWithoutLogsPermission (0.03s)\n```\n\nThe UTF-16LE formatting is from Windows `echo`, but the key result is that the response returned the real command output even though the caller\u0027s ACL explicitly denied log access.\n\n## Impact\n\n- **Bypass of log confidentiality controls:** Operators may configure actions to be executable but not log-readable. The `...AndWait` endpoints break that separation.\n- **Disclosure of secrets printed by actions:** Many OliveTin actions wrap administrative scripts and commands whose stdout/stderr may contain credentials, internal paths, hostnames, tokens, or sensitive operational data.\n- **Adjacent to previous output-leak bugs:** OliveTin already had an EventStream output disclosure bug. This is a separate response-path authorization gap on the synchronous execution APIs.\n\n## Recommended Fix\n\nEnforce `logs` permission before returning `LogEntry` content from synchronous execution endpoints.\n\nTwo reasonable fixes:\n\n1. Deny the endpoint response when `logs:false`\n\n```go\nif !acl.IsAllowedLogs(api.cfg, user, binding.Action) {\n return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf(\"permission denied to view action output\"))\n}\n```\n\n2. Or redact log-bearing fields when `logs:false`\n\n```go\npb := api.internalLogEntryToPb(internalLogEntry, user)\nif !acl.IsAllowedLogs(api.cfg, user, binding.Action) {\n pb.Output = \"\"\n pb.ExitCode = 0\n}\nreturn connect.NewResponse(\u0026apiv1.StartActionAndWaitResponse{LogEntry: pb}), nil\n```\n\nThe same fix should be applied to both:\n\n- `StartActionAndWait`\n- `StartActionByGetAndWait`\n\n## References\n\n- `service/internal/api/api.go`\n- `service/internal/config/config.go`\n- `service/internal/acl/acl.go`",
"id": "GHSA-jm28-2wcr-qf3h",
"modified": "2026-07-30T14:25:20Z",
"published": "2026-07-30T14:25:20Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/OliveTin/OliveTin/security/advisories/GHSA-jm28-2wcr-qf3h"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-67439"
},
{
"type": "WEB",
"url": "https://github.com/OliveTin/OliveTin/commit/e421780c9885aa5024d2f47b4ed4898f2f18eb90"
},
{
"type": "PACKAGE",
"url": "https://github.com/OliveTin/OliveTin"
},
{
"type": "WEB",
"url": "https://github.com/OliveTin/OliveTin/releases/tag/3000.17.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "OliveTin: StartActionAndWait Endpoints Bypass `logs` Permission and Return Action Output"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
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.