{"id":"CVE-2026-67439","aliases":["GHSA-jm28-2wcr-qf3h"],"title":"OliveTin: StartActionAndWait Endpoints Bypass `logs` Permission and Return Action Output","summary":"OliveTin: StartActionAndWait Endpoints Bypass `logs` Permission and Return Action Output","severity":"medium","cvss":4.3,"cwe":["CWE-863"],"vendor":"OliveTin","product":"github.com/OliveTin/OliveTin","ecosystem":"go","affected":["github.com/OliveTin/OliveTin < 0.0.0-20260708085316-e421780c9885"],"patched":["github.com/OliveTin/OliveTin 0.0.0-20260708085316-e421780c9885"],"published":"2026-07-30","updated":"2026-07-30","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-jm28-2wcr-qf3h","references":[{"url":"https://github.com/OliveTin/OliveTin/security/advisories/GHSA-jm28-2wcr-qf3h"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-67439"},{"url":"https://github.com/OliveTin/OliveTin/commit/e421780c9885aa5024d2f47b4ed4898f2f18eb90"},{"url":"https://github.com/OliveTin/OliveTin/releases/tag/3000.17.0"},{"url":"https://github.com/advisories/GHSA-jm28-2wcr-qf3h"}],"tags":["ghsa","go"],"epss":0.00253,"epssPercentile":0.17031,"ingestedAt":"2026-07-30T14:54:20.465Z","slug":"CVE-2026-67439","body":"## Overview\n\n## 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's logs.\n\nOliveTin'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.\n\nAs 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`.\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(&apiv1.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(&apiv1.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 := &apiv1.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's 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(&apiv1.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`\n\n## Affected packages\n\n- `github.com/OliveTin/OliveTin < 0.0.0-20260708085316-e421780c9885`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `github.com/OliveTin/OliveTin 0.0.0-20260708085316-e421780c9885`","depth":"sunlit","depthScore":24,"depthScoreParts":{"impact":23.7,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}