OliveTin: View permission not being checked when returning dashboards
Description
OliveTin gives access to predefined shell commands from a web interface. Prior to version 3000.11.1, an authorization flaw in OliveTin allows authenticated users with view: false permission to enumerate action bindings and metadata via dashboard and API endpoints. Although execution (exec) may be correctly denied, the backend does not enforce IsAllowedView() when constructing dashboard and action binding responses. As a result, restricted users can retrieve action titles, IDs, icons, and argument metadata. This issue has been patched in version 3000.11.1.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
OliveTin before 3000.11.1 fails to enforce view permission, allowing authenticated low-privilege users to enumerate action metadata via dashboard and API.
Vulnerability
OliveTin prior to version 3000.11.1 contains an authorization flaw where the backend does not enforce the IsAllowedView() check when constructing dashboard and action binding responses. This allows authenticated users with view: false permission to enumerate action bindings and metadata via dashboard and API endpoints. Although execution (exec) is correctly denied, the missing view check leaks action titles, IDs, icons, and argument metadata [1][2][3].
Exploitation
An attacker with low-privilege credentials (e.g., a user with view: false and exec: false) can access the dashboard or API endpoints that list actions. The dashboard building logic in dashboards.go and apiActions.go does not call IsAllowedView(), so restricted users receive action metadata even though they should not see any actions [3]. The attack requires only authentication as a user with restricted permissions; no special network position is needed.
Impact
The vulnerability leads to information disclosure of action metadata, including action titles, IDs, icons, descriptions, and argument definitions. This could reveal the existence of sensitive actions, aiding further attacks. However, the attacker cannot execute the actions because exec permission is correctly denied.
Mitigation
The issue has been patched in OliveTin version 3000.11.1. Users should upgrade to this version or later. No workarounds are available; updating is the recommended action [1][2][3].
AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/OliveTin/OliveTinGo | < 0.0.0-20260305082002-d7962710e7c4 | 0.0.0-20260305082002-d7962710e7c4 |
Affected products
2- OliveTin/OliveTinv5Range: < 3000.11.1
Patches
1d7962710e7c4security: GHSA-jf73-858c-54pg (MODERATE) View permission not being checked when returning dashboards
4 files changed · +269 −66
service/internal/api/apiActions.go+33 −18 modified@@ -28,18 +28,22 @@ func (rr *DashboardRenderRequest) findAction(title string) *apiv1.Action { return rr.findActionForEntity(title, nil) } +func bindingMatchesTitleAndEntity(binding *executor.ActionBinding, title string, entity *entities.Entity) bool { + return binding != nil && binding.Action != nil && binding.Action.Title == title && matchesEntity(binding, entity) +} + func (rr *DashboardRenderRequest) findActionForEntity(title string, entity *entities.Entity) *apiv1.Action { rr.ex.MapActionBindingsLock.RLock() defer rr.ex.MapActionBindingsLock.RUnlock() for _, binding := range rr.ex.MapActionBindings { - if binding.Action.Title != title { + if !bindingMatchesTitleAndEntity(binding, title, entity) { continue } - - if matchesEntity(binding, entity) { - return buildAction(binding, rr) + if !acl.IsAllowedView(rr.cfg, rr.AuthenticatedUser, binding.Action) { + return nil } + return buildAction(binding, rr) } return nil @@ -117,26 +121,37 @@ func getDefaultArgumentValue(cfgArg config.ActionArgument, entity *entities.Enti return defaultValue } -func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderRequest) *apiv1.Action { - action := actionBinding.Action +func formatRateLimitExpiry(expiryUnix int64) string { + if expiryUnix <= 0 { + return "" + } + return time.Unix(expiryUnix, 0).Format("2006-01-02 15:04:05") +} - aclCanExec := acl.IsAllowedExec(rr.cfg, rr.AuthenticatedUser, action) - enabledExprCanExec := evaluateEnabledExpression(action, actionBinding.Entity) +func actionFromBinding(actionBinding *executor.ActionBinding) (*executor.ActionBinding, *config.Action) { + if actionBinding == nil || actionBinding.Action == nil { + return nil, nil + } + return actionBinding, actionBinding.Action +} - // Calculate rate limit expiry time - expiryUnix := rr.ex.GetTimeUntilAvailable(actionBinding) - datetimeRateLimitExpires := "" - if expiryUnix > 0 { - datetimeRateLimitExpires = time.Unix(expiryUnix, 0).Format("2006-01-02 15:04:05") +func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderRequest) *apiv1.Action { + binding, action := actionFromBinding(actionBinding) + if binding == nil { + return nil } + aclCanExec := acl.IsAllowedExec(rr.cfg, rr.AuthenticatedUser, action) + enabledExprCanExec := evaluateEnabledExpression(action, binding.Entity) + datetimeRateLimitExpires := formatRateLimitExpiry(rr.ex.GetTimeUntilAvailable(binding)) + btn := apiv1.Action{ - BindingId: actionBinding.ID, - Title: tpl.ParseTemplateOfActionBeforeExec(action.Title, actionBinding.Entity), - Icon: tpl.ParseTemplateOfActionBeforeExec(action.Icon, actionBinding.Entity), + BindingId: binding.ID, + Title: tpl.ParseTemplateOfActionBeforeExec(action.Title, binding.Entity), + Icon: tpl.ParseTemplateOfActionBeforeExec(action.Icon, binding.Entity), CanExec: aclCanExec && enabledExprCanExec, PopupOnStart: action.PopupOnStart, - Order: int32(actionBinding.ConfigOrder), + Order: int32(binding.ConfigOrder), Timeout: int32(action.Timeout), DatetimeRateLimitExpires: datetimeRateLimitExpires, } @@ -147,7 +162,7 @@ func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderReque Title: cfgArg.Title, Type: cfgArg.Type, Description: cfgArg.Description, - DefaultValue: getDefaultArgumentValue(cfgArg, actionBinding.Entity), + DefaultValue: getDefaultArgumentValue(cfgArg, binding.Entity), Choices: buildChoices(cfgArg), Suggestions: cfgArg.Suggestions, SuggestionsBrowserKey: cfgArg.SuggestionsBrowserKey,
service/internal/api/api.go+112 −47 modified@@ -70,20 +70,21 @@ func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *connect.Request[apiv1.K execReqLogEntry, ret.Found = api.executor.GetLog(req.Msg.ExecutionTrackingId) if !ret.Found { - log.Warnf("Killing execution request not possible - not found by tracking ID: %v", req.Msg.ExecutionTrackingId) - return connect.NewResponse(ret), nil + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found for tracking ID %s", req.Msg.ExecutionTrackingId)) } - log.Warnf("Killing execution request by tracking ID: %v", req.Msg.ExecutionTrackingId) + if execReqLogEntry.Binding == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("log entry has no binding for tracking ID %s", req.Msg.ExecutionTrackingId)) + } action := execReqLogEntry.Binding.Action if action == nil { - log.Warnf("Killing execution request not possible - action not found: %v", execReqLogEntry.ActionTitle) - ret.Killed = false - return connect.NewResponse(ret), nil + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action not found for tracking ID %s", req.Msg.ExecutionTrackingId)) } + log.Warnf("Killing execution request by tracking ID: %v", req.Msg.ExecutionTrackingId) + user := auth.UserFromApiCall(ctx, req, api.cfg) api.killActionByTrackingId(user, action, execReqLogEntry, ret) @@ -205,42 +206,58 @@ func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[api return response, nil } -func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionAndWaitRequest]) (*connect.Response[apiv1.StartActionAndWaitResponse], error) { - args := make(map[string]string) - - for _, arg := range req.Msg.Arguments { - args[arg.Name] = arg.Value - } - - user := auth.UserFromApiCall(ctx, req, api.cfg) - +func (api *oliveTinAPI) startActionAndWaitRun(binding *executor.ActionBinding, args map[string]string, user *authpublic.AuthenticatedUser) (*executor.InternalLogEntry, bool) { execReq := executor.ExecutionRequest{ - Binding: api.executor.FindBindingByID(req.Msg.ActionId), + Binding: binding, TrackingID: uuid.NewString(), Arguments: args, AuthenticatedUser: user, Cfg: api.cfg, } - wg, _ := api.executor.ExecRequest(&execReq) wg.Wait() + return api.executor.GetLog(execReq.TrackingID) +} - internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID) +func (api *oliveTinAPI) findBindingOrNotFound(actionId string) (*executor.ActionBinding, error) { + binding := api.executor.FindBindingByID(actionId) + if binding == nil || binding.Action == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", actionId)) + } + return binding, nil +} - if ok { - return connect.NewResponse(&apiv1.StartActionAndWaitResponse{ - LogEntry: api.internalLogEntryToPb(internalLogEntry, user), - }), nil - } else { - return nil, fmt.Errorf("execution not found") +func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionAndWaitRequest]) (*connect.Response[apiv1.StartActionAndWaitResponse], error) { + binding, err := api.findBindingOrNotFound(req.Msg.ActionId) + if err != nil { + return nil, err + } + + args := make(map[string]string) + for _, arg := range req.Msg.Arguments { + args[arg.Name] = arg.Value } + user := auth.UserFromApiCall(ctx, req, api.cfg) + + 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) StartActionByGet(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetRequest]) (*connect.Response[apiv1.StartActionByGetResponse], error) { + binding := api.executor.FindBindingByID(req.Msg.ActionId) + if binding == nil || binding.Action == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.ActionId)) + } + args := make(map[string]string) execReq := executor.ExecutionRequest{ - Binding: api.executor.FindBindingByID(req.Msg.ActionId), + Binding: binding, TrackingID: uuid.NewString(), Arguments: args, AuthenticatedUser: auth.UserFromApiCall(ctx, req, api.cfg), @@ -255,12 +272,17 @@ func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *connect.Request[a } func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetAndWaitRequest]) (*connect.Response[apiv1.StartActionByGetAndWaitResponse], error) { + binding := api.executor.FindBindingByID(req.Msg.ActionId) + if binding == nil || binding.Action == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.ActionId)) + } + args := make(map[string]string) user := auth.UserFromApiCall(ctx, req, api.cfg) execReq := executor.ExecutionRequest{ - Binding: api.executor.FindBindingByID(req.Msg.ActionId), + Binding: binding, TrackingID: uuid.NewString(), Arguments: args, AuthenticatedUser: user, @@ -276,9 +298,8 @@ func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Re return connect.NewResponse(&apiv1.StartActionByGetAndWaitResponse{ LogEntry: api.internalLogEntryToPb(internalLogEntry, user), }), nil - } else { - return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found")) } + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found")) } func calculateRateLimitExpires(api *oliveTinAPI, logEntry *executor.InternalLogEntry) string { @@ -392,6 +413,8 @@ func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[ap func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.LogoutRequest]) (*connect.Response[apiv1.LogoutResponse], error) { user := auth.UserFromApiCall(ctx, req, api.cfg) + auth.RevokeSessionForProvider(api.cfg, user.Provider, user.SID) + log.WithFields(log.Fields{ "username": user.Username, "provider": user.Provider, @@ -434,19 +457,38 @@ func (api *oliveTinAPI) GetActionBinding(ctx ctx.Context, req *connect.Request[a return nil, err } - binding := api.executor.FindBindingByID(req.Msg.BindingId) + resp, err := api.getActionBindingResponse(user, req.Msg.BindingId) + if err != nil { + return nil, err + } + return connect.NewResponse(resp), nil +} +func (api *oliveTinAPI) getActionBindingResponse(user *authpublic.AuthenticatedUser, bindingId string) (*apiv1.GetActionBindingResponse, error) { + binding := api.executor.FindBindingByID(bindingId) if binding == nil { - return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.BindingId)) + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", bindingId)) } - - return connect.NewResponse(&apiv1.GetActionBindingResponse{ + if binding.Action == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", bindingId)) + } + if !api.userCanViewAction(user, binding.Action) { + return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied")) + } + return &apiv1.GetActionBindingResponse{ Action: buildAction(binding, &DashboardRenderRequest{ cfg: api.cfg, AuthenticatedUser: user, ex: api.executor, }), - }), nil + }, nil +} + +func (api *oliveTinAPI) userCanViewAction(user *authpublic.AuthenticatedUser, action *config.Action) bool { + if user == nil { + return true + } + return acl.IsAllowedView(api.cfg, user, action) } func (api *oliveTinAPI) GetDashboard(ctx ctx.Context, req *connect.Request[apiv1.GetDashboardRequest]) (*connect.Response[apiv1.GetDashboardResponse], error) { @@ -646,7 +688,16 @@ error messages more quickly before starting the action. It uses the same validation logic as the executor, including mangling argument values (e.g., datetime formatting, checkbox title-to-value conversion). */ +func (api *oliveTinAPI) argumentNotFoundForValidation(msg *apiv1.ValidateArgumentTypeRequest) bool { + arg, _ := api.findArgumentForValidation(msg.BindingId, msg.ArgumentName) + return arg == nil && (msg.BindingId != "" || msg.ArgumentName != "") +} + func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *connect.Request[apiv1.ValidateArgumentTypeRequest]) (*connect.Response[apiv1.ValidateArgumentTypeResponse], error) { + if api.argumentNotFoundForValidation(req.Msg) { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action or argument not found for binding ID %s", req.Msg.BindingId)) + } + err := api.validateArgumentTypeInternal(req.Msg) desc := "" if err != nil { @@ -747,6 +798,13 @@ func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *connect.Request[apiv1.Dum return connect.NewResponse(res), nil } +func debugBindingActionTitle(binding *executor.ActionBinding) string { + if binding == nil || binding.Action == nil { + return "" + } + return binding.Action.Title +} + func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *connect.Request[apiv1.DumpPublicIdActionMapRequest]) (*connect.Response[apiv1.DumpPublicIdActionMapResponse], error) { res := &apiv1.DumpPublicIdActionMapResponse{} res.Contents = make(map[string]*apiv1.DebugBinding) @@ -761,7 +819,7 @@ func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *connect.Requ for k, v := range api.executor.MapActionBindings { res.Contents[k] = &apiv1.DebugBinding{ - ActionTitle: v.Action.Title, + ActionTitle: debugBindingActionTitle(v), } } @@ -1271,30 +1329,37 @@ func (api *oliveTinAPI) RestartAction(ctx ctx.Context, req *connect.Request[apiv ExecutionTrackingId: req.Msg.ExecutionTrackingId, } - var execReqLogEntry *executor.InternalLogEntry - execReqLogEntry, found := api.executor.GetLog(req.Msg.ExecutionTrackingId) if !found { - log.Warnf("Restarting execution request not possible - not found by tracking ID: %v", req.Msg.ExecutionTrackingId) - return connect.NewResponse(ret), nil + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found for tracking ID %s", req.Msg.ExecutionTrackingId)) } - log.Warnf("Restarting execution request by tracking ID: %v", req.Msg.ExecutionTrackingId) + if execReqLogEntry.Binding == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("log entry has no binding for tracking ID %s", req.Msg.ExecutionTrackingId)) + } action := execReqLogEntry.Binding.Action if action == nil { - log.Warnf("Restarting execution request not possible - action not found: %v", execReqLogEntry.ActionTitle) - return connect.NewResponse(ret), nil + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action not found for tracking ID %s", req.Msg.ExecutionTrackingId)) } - return api.StartAction(ctx, &connect.Request[apiv1.StartActionRequest]{ - Msg: &apiv1.StartActionRequest{ - BindingId: execReqLogEntry.GetBindingId(), - UniqueTrackingId: req.Msg.ExecutionTrackingId, - }, - }) + authenticatedUser := auth.UserFromApiCall(ctx, req, api.cfg) + + // TrackingID is deliberately not passed to the executor, so that it generates a new one for the restarted execution. + // This is because the old execution (identified by the old TrackingID) is already used. + execReq := executor.ExecutionRequest{ + Binding: execReqLogEntry.Binding, + Arguments: make(map[string]string), + AuthenticatedUser: authenticatedUser, + Cfg: api.cfg, + } + + api.executor.ExecRequest(&execReq) + + ret.ExecutionTrackingId = execReq.TrackingID + return connect.NewResponse(ret), nil } func newServer(ex *executor.Executor) *oliveTinAPI {
service/internal/api/api_test.go+115 −0 modified@@ -6,6 +6,7 @@ import ( "connectrpc.com/connect" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" log "github.com/sirupsen/logrus" @@ -335,3 +336,117 @@ func testWithEntity(t *testing.T, binding *executor.ActionBinding, rr *Dashboard actionResult := buildAction(binding, rr) assert.Equal(t, expectedCanExec, actionResult.CanExec, message) } + +// buildViewPermissionTestConfig returns config and users for GHSA view-permission tests: +// one action "secret_action", ACL "restricted" (view:false) for user "low", ACL "full" (view:true) for user "admin". +func buildViewPermissionTestConfig(t *testing.T) (*config.Config, *authpublic.AuthenticatedUser, *authpublic.AuthenticatedUser) { + t.Helper() + cfg := config.DefaultConfig() + cfg.DefaultPermissions.View = false + cfg.DefaultPermissions.Exec = false + + cfg.Actions = append(cfg.Actions, &config.Action{ + ID: "secret_action", + Title: "Secret Action", + Shell: "echo sensitive", + Icon: "🔒", + }) + + cfg.AccessControlLists = append(cfg.AccessControlLists, + &config.AccessControlList{ + Name: "restricted", + MatchUsernames: []string{"low"}, + AddToEveryAction: true, + Permissions: config.PermissionsList{View: false, Exec: false, Logs: false, Kill: false}, + }, + &config.AccessControlList{ + Name: "full", + MatchUsernames: []string{"admin"}, + AddToEveryAction: true, + Permissions: config.PermissionsList{View: true, Exec: true, Logs: true, Kill: true}, + }, + ) + + lowUser := &authpublic.AuthenticatedUser{Username: "low"} + lowUser.BuildUserAcls(cfg) + adminUser := &authpublic.AuthenticatedUser{Username: "admin"} + adminUser.BuildUserAcls(cfg) + return cfg, lowUser, adminUser +} + +// TestViewPermissionExcludedFromDashboard (GHSA: view permission) asserts that when a user has view: false, +// the default dashboard must not include that action. Covers GetDashboard not leaking action metadata. +func TestViewPermissionExcludedFromDashboard(t *testing.T) { + cfg, lowUser, _ := buildViewPermissionTestConfig(t) + ex := executor.DefaultExecutor(cfg) + ex.RebuildActionMap() + + rr := &DashboardRenderRequest{ + AuthenticatedUser: lowUser, + cfg: cfg, + ex: ex, + } + db := buildDefaultDashboard(rr) + + bindingIdsInDashboard := bindingIdsInDashboardContents(db.Contents) + assert.NotContains(t, bindingIdsInDashboard, "secret_action", + "user with view:false must not see action in dashboard; got bindingIds: %v", bindingIdsInDashboard) +} + +// TestGetActionBindingDeniedWhenNoViewPermission (GHSA: view permission) asserts that GetActionBinding +// returns permission denied for a user with view: false. Covers GetActionBinding not exposing action details. +func TestGetActionBindingDeniedWhenNoViewPermission(t *testing.T) { + cfg, lowUser, _ := buildViewPermissionTestConfig(t) + ex := executor.DefaultExecutor(cfg) + ex.RebuildActionMap() + api := newServer(ex) + + _, err := api.getActionBindingResponse(lowUser, "secret_action") + require.Error(t, err) + assert.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err), + "user with view:false must get permission denied from GetActionBinding") +} + +// TestViewPermissionAllowedSeesAction (GHSA: view permission) asserts that a user with view: true +// still sees the action in the dashboard and can fetch it via GetActionBinding. +func TestViewPermissionAllowedSeesAction(t *testing.T) { + cfg, _, adminUser := buildViewPermissionTestConfig(t) + ex := executor.DefaultExecutor(cfg) + ex.RebuildActionMap() + api := newServer(ex) + + rr := &DashboardRenderRequest{ + AuthenticatedUser: adminUser, + cfg: cfg, + ex: ex, + } + db := buildDefaultDashboard(rr) + bindingIdsInDashboard := bindingIdsInDashboardContents(db.Contents) + assert.Contains(t, bindingIdsInDashboard, "secret_action", + "user with view:true must see action in dashboard; got bindingIds: %v", bindingIdsInDashboard) + + resp, err := api.getActionBindingResponse(adminUser, "secret_action") + require.NoError(t, err) + require.NotNil(t, resp) + require.NotNil(t, resp.Action) + assert.Equal(t, "secret_action", resp.Action.BindingId) +} + +func bindingIdsInDashboardContents(contents []*apiv1.DashboardComponent) []string { + var ids []string + for _, c := range contents { + ids = append(ids, bindingIdsFromComponent(c)...) + } + return ids +} + +func bindingIdsFromComponent(c *apiv1.DashboardComponent) []string { + if c == nil { + return nil + } + var ids []string + if c.Action != nil && c.Action.BindingId != "" { + ids = append(ids, c.Action.BindingId) + } + return append(ids, bindingIdsInDashboardContents(c.Contents)...) +}
service/internal/api/dashboards.go+9 −1 modified@@ -4,6 +4,7 @@ import ( "sort" apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1" + acl "github.com/OliveTin/OliveTin/internal/acl" config "github.com/OliveTin/OliveTin/internal/config" entities "github.com/OliveTin/OliveTin/internal/entities" "github.com/OliveTin/OliveTin/internal/tpl" @@ -130,15 +131,22 @@ func buildDefaultDashboard(rr *DashboardRenderRequest) *apiv1.Dashboard { } for _, binding := range rr.ex.MapActionBindings { - if binding.Action.Hidden { + if binding == nil || binding.Action == nil || binding.Action.Hidden { continue } if binding.IsOnDashboard { continue } + if !acl.IsAllowedView(rr.cfg, rr.AuthenticatedUser, binding.Action) { + continue + } + action := buildAction(binding, rr) + if action == nil { + continue + } fieldset.Contents = append(fieldset.Contents, &apiv1.DashboardComponent{ Type: "link",
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-jf73-858c-54pgghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-30233ghsaADVISORY
- github.com/OliveTin/OliveTin/commit/d7962710e7c46f6bdda4188b5b0cdbde4be665a0ghsax_refsource_MISCWEB
- github.com/OliveTin/OliveTin/releases/tag/3000.11.1ghsax_refsource_MISCWEB
- github.com/OliveTin/OliveTin/security/advisories/GHSA-jf73-858c-54pgghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.