GHSA-jm28-2wcr-qf3h
MEDIUMGHSA-jm28-2wcr-qf3h is a medium-severity (CVSS 4.3) CWE-863 vulnerability in github.com/OliveTin/OliveTin. O3 Security confirms whether GHSA-jm28-2wcr-qf3h is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
OliveTin: StartActionAndWait Endpoints Bypass `logs` Permission and Return Action Output
Blast Radius
github.com/OliveTin/OliveTinReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.
Description
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:
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 Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐹Go | github.com/OliveTin/OliveTin | all versions | 0.0.0-20260708085316-e421780c9885 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/OliveTin/OliveTin. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update github.com/OliveTin/OliveTin to 0.0.0-20260708085316-e421780c9885 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-jm28-2wcr-qf3h is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-jm28-2wcr-qf3h is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-jm28-2wcr-qf3h. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-jm28-2wcr-qf3h in your dependencies?
O3 detects GHSA-jm28-2wcr-qf3h across Go dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.