GHSA-JM28-2WCR-QF3H

Vulnerability from github – Published: 2026-07-30 14:25 – Updated: 2026-07-30 14:25
VLAI
Summary
OliveTin: StartActionAndWait Endpoints Bypass `logs` Permission and Return Action Output
Details

Summary

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:

  • GetLogs
  • GetActionLogs
  • ExecutionStatus
  • EventStream

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_action with shell echo SECRET_FROM_ACTION
  • user low matched by ACL:
  • exec: true
  • logs: false
  • view: false
  • kill: 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 ...AndWait endpoints 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:

  1. 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"))
}
  1. 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:

  • StartActionAndWait
  • StartActionByGetAndWait

References

  • service/internal/api/api.go
  • service/internal/config/config.go
  • service/internal/acl/acl.go
Show details on source website

{
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…