Grafana is an open-source platform for monitoring and observability.

The option to send a test alert is not available from the user panel UI for users having the Viewer role. It is still possible for a user with the Viewer role to send a test alert using the API as the API does not check access to this function.

This might enable malicious users to abuse the functionality by sending multiple alert messages to e-mail and Slack, spamming users, prepare Phishing attack or block SMTP server.

Users may upgrade to version 9.5.3, 9.4.12, 9.3.15, 9.2.19 and 8.5.26 to receive a fix.

Missing Authorization

The product does not perform an authorization check when an actor attempts to access a resource or perform an action.

func (api *API) authorize(method, path string) web.Handler {
	authorize := ac.Middleware(api.AccessControl)
	var eval ac.Evaluator = nil

	// Most routes follow this general authorization approach as a fallback. Exceptions are overridden directly in the below block.
	var fallback web.Handler
	switch method {
	case http.MethodPost, http.MethodPut, http.MethodDelete:
		fallback = middleware.ReqEditorRole
	case http.MethodGet:
		fallback = middleware.ReqSignedIn
	default:
		fallback = middleware.ReqSignedIn
	}

	switch method + path {
	// Alert Rules

	// Grafana Paths
	case http.MethodDelete + "/api/ruler/grafana/api/v1/rules/{Namespace}/{Groupname}":
		eval = ac.EvalPermission(ac.ActionAlertingRuleDelete, dashboards.ScopeFoldersProvider.GetResourceScopeName(ac.Parameter(":Namespace")))
	case http.MethodDelete + "/api/ruler/grafana/api/v1/rules/{Namespace}":
		eval = ac.EvalPermission(ac.ActionAlertingRuleDelete, dashboards.ScopeFoldersProvider.GetResourceScopeName(ac.Parameter(":Namespace")))
	case http.MethodGet + "/api/ruler/grafana/api/v1/rules/{Namespace}/{Groupname}":
		eval = ac.EvalPermission(ac.ActionAlertingRuleRead, dashboards.ScopeFoldersProvider.GetResourceScopeName(ac.Parameter(":Namespace")))
	case http.MethodGet + "/api/ruler/grafana/api/v1/rules/{Namespace}":
		eval = ac.EvalPermission(ac.ActionAlertingRuleRead, dashboards.ScopeFoldersProvider.GetResourceScopeName(ac.Parameter(":Namespace")))
	case http.MethodGet + "/api/ruler/grafana/api/v1/rules":
		eval = ac.EvalPermission(ac.ActionAlertingRuleRead)
	case http.MethodPost + "/api/ruler/grafana/api/v1/rules/{Namespace}":
		fallback = middleware.ReqSignedIn // if RBAC is disabled then we need to delegate permission check to folder because its permissions can allow editing for Viewer role
		scope := dashboards.ScopeFoldersProvider.GetResourceScopeName(ac.Parameter(":Namespace"))
		// more granular permissions are enforced by the handler via "authorizeRuleChanges"
		eval = ac.EvalAny(
			ac.EvalPermission(ac.ActionAlertingRuleUpdate, scope),
			ac.EvalPermission(ac.ActionAlertingRuleCreate, scope),
			ac.EvalPermission(ac.ActionAlertingRuleDelete, scope),
		)
	// Grafana rule state history paths
	case http.MethodGet + "/api/v1/rules/history":
		fallback = middleware.ReqSignedIn
		eval = ac.EvalPermission(ac.ActionAlertingRuleRead)

	// Grafana, Prometheus-compatible Paths
	case http.MethodGet + "/api/prometheus/grafana/api/v1/rules":
		eval = ac.EvalPermission(ac.ActionAlertingRuleRead)

	// Grafana Rules Testing Paths
	case http.MethodPost + "/api/v1/rule/test/grafana":
		fallback = middleware.ReqSignedIn
		// additional authorization is done in the request handler
		eval = ac.EvalPermission(ac.ActionAlertingRuleRead)
	// Grafana Rules Testing Paths
	case http.MethodPost + "/api/v1/rule/backtest":
		fallback = middleware.ReqSignedIn
		// additional authorization is done in the request handler
		eval = ac.EvalPermission(ac.ActionAlertingRuleRead)
	case http.MethodPost + "/api/v1/eval":
		fallback = middleware.ReqSignedIn
		// additional authorization is done in the request handler
		eval = ac.EvalPermission(ac.ActionAlertingRuleRead)

	// Lotex Paths
	case http.MethodDelete + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}":
		eval = ac.EvalPermission(ac.ActionAlertingRuleExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodDelete + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname}":
		eval = ac.EvalPermission(ac.ActionAlertingRuleExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodGet + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}":
		eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodGet + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname}":
		eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodGet + "/api/ruler/{DatasourceUID}/api/v1/rules":
		eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodPost + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}":
		eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))

	// Lotex Prometheus-compatible Paths
	case http.MethodGet + "/api/prometheus/{DatasourceUID}/api/v1/rules":
		eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))

	// Lotex Rules testing
	case http.MethodPost + "/api/v1/rule/test/{DatasourceUID}":
		fallback = middleware.ReqSignedIn
		eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))

	// Alert Instances and Silences

	// Silences. Grafana Paths
	case http.MethodDelete + "/api/alertmanager/grafana/api/v2/silence/{SilenceId}":
		eval = ac.EvalPermission(ac.ActionAlertingInstanceUpdate) // delete endpoint actually expires silence
	case http.MethodGet + "/api/alertmanager/grafana/api/v2/silence/{SilenceId}":
		eval = ac.EvalPermission(ac.ActionAlertingInstanceRead)
	case http.MethodGet + "/api/alertmanager/grafana/api/v2/silences":
		eval = ac.EvalPermission(ac.ActionAlertingInstanceRead)
	case http.MethodPost + "/api/alertmanager/grafana/api/v2/silences":
		// additional authorization is done in the request handler
		eval = ac.EvalAny(ac.EvalPermission(ac.ActionAlertingInstanceCreate), ac.EvalPermission(ac.ActionAlertingInstanceUpdate))

	// Alert Instances. Grafana Paths
	case http.MethodGet + "/api/alertmanager/grafana/api/v2/alerts/groups":
		eval = ac.EvalPermission(ac.ActionAlertingInstanceRead)
	case http.MethodGet + "/api/alertmanager/grafana/api/v2/alerts":
		eval = ac.EvalPermission(ac.ActionAlertingInstanceRead)

	// Grafana Prometheus-compatible Paths
	case http.MethodGet + "/api/prometheus/grafana/api/v1/alerts":
		eval = ac.EvalPermission(ac.ActionAlertingInstanceRead)

	// Silences. External AM.
	case http.MethodDelete + "/api/alertmanager/{DatasourceUID}/api/v2/silence/{SilenceId}":
		eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodPost + "/api/alertmanager/{DatasourceUID}/api/v2/silences":
		eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodGet + "/api/alertmanager/{DatasourceUID}/api/v2/silence/{SilenceId}":
		eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodGet + "/api/alertmanager/{DatasourceUID}/api/v2/silences":
		eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))

	// Alert instances. External AM.
	case http.MethodGet + "/api/alertmanager/{DatasourceUID}/api/v2/alerts/groups":
		eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodGet + "/api/alertmanager/{DatasourceUID}/api/v2/alerts":
		eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodPost + "/api/alertmanager/{DatasourceUID}/api/v2/alerts":
		eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))

	// Prometheus-compatible Paths
	case http.MethodGet + "/api/prometheus/{DatasourceUID}/api/v1/alerts":
		eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))

	// Notification Policies, Contact Points and Templates

	// Grafana Paths
	case http.MethodDelete + "/api/alertmanager/grafana/config/api/v1/alerts": // reset alertmanager config to the default
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsWrite)
	case http.MethodGet + "/api/alertmanager/grafana/config/api/v1/alerts":
		fallback = middleware.ReqEditorRole
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
	case http.MethodGet + "/api/alertmanager/grafana/config/history":
		fallback = middleware.ReqEditorRole
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
	case http.MethodGet + "/api/alertmanager/grafana/api/v2/status":
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
	case http.MethodPost + "/api/alertmanager/grafana/config/api/v1/alerts":
		// additional authorization is done in the request handler
		eval = ac.EvalAny(ac.EvalPermission(ac.ActionAlertingNotificationsWrite))
	case http.MethodGet + "/api/alertmanager/grafana/config/api/v1/receivers":
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
	case http.MethodPost + "/api/alertmanager/grafana/config/api/v1/receivers/test":
		fallback = middleware.ReqEditorRole
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)

	// External Alertmanager Paths
	case http.MethodDelete + "/api/alertmanager/{DatasourceUID}/config/api/v1/alerts":
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodGet + "/api/alertmanager/{DatasourceUID}/api/v2/status":
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodGet + "/api/alertmanager/{DatasourceUID}/config/api/v1/alerts":
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodPost + "/api/alertmanager/{DatasourceUID}/config/api/v1/alerts":
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))

	case http.MethodGet + "/api/v1/ngalert":
		// let user with any alerting permission access this API
		eval = ac.EvalAny(
			ac.EvalPermission(ac.ActionAlertingInstanceRead),
			ac.EvalPermission(ac.ActionAlertingInstancesExternalRead),
			ac.EvalPermission(ac.ActionAlertingRuleRead),
			ac.EvalPermission(ac.ActionAlertingRuleExternalRead),
			ac.EvalPermission(ac.ActionAlertingNotificationsRead),
			ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead),
		)
	// Raw Alertmanager Config Paths
	case http.MethodDelete + "/api/v1/ngalert/admin_config",
		http.MethodGet + "/api/v1/ngalert/admin_config",
		http.MethodPost + "/api/v1/ngalert/admin_config",
		http.MethodGet + "/api/v1/ngalert/alertmanagers":
		return middleware.ReqOrgAdmin

	// Grafana-only Provisioning Read Paths
	case http.MethodGet + "/api/v1/provisioning/policies",
		http.MethodGet + "/api/v1/provisioning/contact-points",
		http.MethodGet + "/api/v1/provisioning/templates",
		http.MethodGet + "/api/v1/provisioning/templates/{name}",
		http.MethodGet + "/api/v1/provisioning/mute-timings",
		http.MethodGet + "/api/v1/provisioning/mute-timings/{name}",
		http.MethodGet + "/api/v1/provisioning/alert-rules",
		http.MethodGet + "/api/v1/provisioning/alert-rules/{UID}",
		http.MethodGet + "/api/v1/provisioning/alert-rules/export",
		http.MethodGet + "/api/v1/provisioning/alert-rules/{UID}/export",
		http.MethodGet + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}",
		http.MethodGet + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export":
		fallback = middleware.ReqOrgAdmin
		eval = ac.EvalPermission(ac.ActionAlertingProvisioningRead) // organization scope

	case http.MethodPut + "/api/v1/provisioning/policies",
		http.MethodDelete + "/api/v1/provisioning/policies",
		http.MethodPost + "/api/v1/provisioning/contact-points",
		http.MethodPut + "/api/v1/provisioning/contact-points/{UID}",
		http.MethodDelete + "/api/v1/provisioning/contact-points/{UID}",
		http.MethodPut + "/api/v1/provisioning/templates/{name}",
		http.MethodDelete + "/api/v1/provisioning/templates/{name}",
		http.MethodPost + "/api/v1/provisioning/mute-timings",
		http.MethodPut + "/api/v1/provisioning/mute-timings/{name}",
		http.MethodDelete + "/api/v1/provisioning/mute-timings/{name}",
		http.MethodPost + "/api/v1/provisioning/alert-rules",
		http.MethodPut + "/api/v1/provisioning/alert-rules/{UID}",
		http.MethodDelete + "/api/v1/provisioning/alert-rules/{UID}",
		http.MethodPut + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}":
		fallback = middleware.ReqOrgAdmin
		eval = ac.EvalPermission(ac.ActionAlertingProvisioningWrite) // organization scope
	}

	if eval != nil {
		return authorize(fallback, eval)
	}

	panic(fmt.Sprintf("no authorization handler for method [%s] of endpoint [%s]", method, path))
}

func (api *API) authorize(method, path string) web.Handler {
	authorize := ac.Middleware(api.AccessControl)
	var eval ac.Evaluator = nil

	// Most routes follow this general authorization approach as a fallback. Exceptions are overridden directly in the below block.
	var fallback web.Handler
	switch method {
	case http.MethodPost, http.MethodPut, http.MethodDelete:
		fallback = middleware.ReqEditorRole
	case http.MethodGet:
		fallback = middleware.ReqSignedIn
	default:
		fallback = middleware.ReqSignedIn
	}

	switch method + path {
	// Alert Rules

	// Grafana Paths
	case http.MethodDelete + "/api/ruler/grafana/api/v1/rules/{Namespace}/{Groupname}":
		eval = ac.EvalPermission(ac.ActionAlertingRuleDelete, dashboards.ScopeFoldersProvider.GetResourceScopeName(ac.Parameter(":Namespace")))
	case http.MethodDelete + "/api/ruler/grafana/api/v1/rules/{Namespace}":
		eval = ac.EvalPermission(ac.ActionAlertingRuleDelete, dashboards.ScopeFoldersProvider.GetResourceScopeName(ac.Parameter(":Namespace")))
	case http.MethodGet + "/api/ruler/grafana/api/v1/rules/{Namespace}/{Groupname}":
		eval = ac.EvalPermission(ac.ActionAlertingRuleRead, dashboards.ScopeFoldersProvider.GetResourceScopeName(ac.Parameter(":Namespace")))
	case http.MethodGet + "/api/ruler/grafana/api/v1/rules/{Namespace}":
		eval = ac.EvalPermission(ac.ActionAlertingRuleRead, dashboards.ScopeFoldersProvider.GetResourceScopeName(ac.Parameter(":Namespace")))
	case http.MethodGet + "/api/ruler/grafana/api/v1/rules":
		eval = ac.EvalPermission(ac.ActionAlertingRuleRead)
	case http.MethodPost + "/api/ruler/grafana/api/v1/rules/{Namespace}":
		fallback = middleware.ReqSignedIn // if RBAC is disabled then we need to delegate permission check to folder because its permissions can allow editing for Viewer role
		scope := dashboards.ScopeFoldersProvider.GetResourceScopeName(ac.Parameter(":Namespace"))
		// more granular permissions are enforced by the handler via "authorizeRuleChanges"
		eval = ac.EvalAny(
			ac.EvalPermission(ac.ActionAlertingRuleUpdate, scope),
			ac.EvalPermission(ac.ActionAlertingRuleCreate, scope),
			ac.EvalPermission(ac.ActionAlertingRuleDelete, scope),
		)
	// Grafana rule state history paths
	case http.MethodGet + "/api/v1/rules/history":
		fallback = middleware.ReqSignedIn
		eval = ac.EvalPermission(ac.ActionAlertingRuleRead)

	// Grafana, Prometheus-compatible Paths
	case http.MethodGet + "/api/prometheus/grafana/api/v1/rules":
		eval = ac.EvalPermission(ac.ActionAlertingRuleRead)

	// Grafana Rules Testing Paths
	case http.MethodPost + "/api/v1/rule/test/grafana":
		fallback = middleware.ReqSignedIn
		// additional authorization is done in the request handler
		eval = ac.EvalPermission(ac.ActionAlertingRuleRead)
	// Grafana Rules Testing Paths
	case http.MethodPost + "/api/v1/rule/backtest":
		fallback = middleware.ReqSignedIn
		// additional authorization is done in the request handler
		eval = ac.EvalPermission(ac.ActionAlertingRuleRead)
	case http.MethodPost + "/api/v1/eval":
		fallback = middleware.ReqSignedIn
		// additional authorization is done in the request handler
		eval = ac.EvalPermission(ac.ActionAlertingRuleRead)

	// Lotex Paths
	case http.MethodDelete + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}":
		eval = ac.EvalPermission(ac.ActionAlertingRuleExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodDelete + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname}":
		eval = ac.EvalPermission(ac.ActionAlertingRuleExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodGet + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}":
		eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodGet + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname}":
		eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodGet + "/api/ruler/{DatasourceUID}/api/v1/rules":
		eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodPost + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}":
		eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))

	// Lotex Prometheus-compatible Paths
	case http.MethodGet + "/api/prometheus/{DatasourceUID}/api/v1/rules":
		eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))

	// Lotex Rules testing
	case http.MethodPost + "/api/v1/rule/test/{DatasourceUID}":
		fallback = middleware.ReqSignedIn
		eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))

	// Alert Instances and Silences

	// Silences. Grafana Paths
	case http.MethodDelete + "/api/alertmanager/grafana/api/v2/silence/{SilenceId}":
		eval = ac.EvalPermission(ac.ActionAlertingInstanceUpdate) // delete endpoint actually expires silence
	case http.MethodGet + "/api/alertmanager/grafana/api/v2/silence/{SilenceId}":
		eval = ac.EvalPermission(ac.ActionAlertingInstanceRead)
	case http.MethodGet + "/api/alertmanager/grafana/api/v2/silences":
		eval = ac.EvalPermission(ac.ActionAlertingInstanceRead)
	case http.MethodPost + "/api/alertmanager/grafana/api/v2/silences":
		// additional authorization is done in the request handler
		eval = ac.EvalAny(ac.EvalPermission(ac.ActionAlertingInstanceCreate), ac.EvalPermission(ac.ActionAlertingInstanceUpdate))

	// Alert Instances. Grafana Paths
	case http.MethodGet + "/api/alertmanager/grafana/api/v2/alerts/groups":
		eval = ac.EvalPermission(ac.ActionAlertingInstanceRead)
	case http.MethodGet + "/api/alertmanager/grafana/api/v2/alerts":
		eval = ac.EvalPermission(ac.ActionAlertingInstanceRead)

	// Grafana Prometheus-compatible Paths
	case http.MethodGet + "/api/prometheus/grafana/api/v1/alerts":
		eval = ac.EvalPermission(ac.ActionAlertingInstanceRead)

	// Silences. External AM.
	case http.MethodDelete + "/api/alertmanager/{DatasourceUID}/api/v2/silence/{SilenceId}":
		eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodPost + "/api/alertmanager/{DatasourceUID}/api/v2/silences":
		eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodGet + "/api/alertmanager/{DatasourceUID}/api/v2/silence/{SilenceId}":
		eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodGet + "/api/alertmanager/{DatasourceUID}/api/v2/silences":
		eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))

	// Alert instances. External AM.
	case http.MethodGet + "/api/alertmanager/{DatasourceUID}/api/v2/alerts/groups":
		eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodGet + "/api/alertmanager/{DatasourceUID}/api/v2/alerts":
		eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodPost + "/api/alertmanager/{DatasourceUID}/api/v2/alerts":
		eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))

	// Prometheus-compatible Paths
	case http.MethodGet + "/api/prometheus/{DatasourceUID}/api/v1/alerts":
		eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))

	// Notification Policies, Contact Points and Templates

	// Grafana Paths
	case http.MethodDelete + "/api/alertmanager/grafana/config/api/v1/alerts": // reset alertmanager config to the default
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsWrite)
	case http.MethodGet + "/api/alertmanager/grafana/config/api/v1/alerts":
		fallback = middleware.ReqEditorRole
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
	case http.MethodGet + "/api/alertmanager/grafana/config/history":
		fallback = middleware.ReqEditorRole
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
	case http.MethodGet + "/api/alertmanager/grafana/api/v2/status":
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
	case http.MethodPost + "/api/alertmanager/grafana/config/api/v1/alerts":
		// additional authorization is done in the request handler
		eval = ac.EvalAny(ac.EvalPermission(ac.ActionAlertingNotificationsWrite))
	case http.MethodGet + "/api/alertmanager/grafana/config/api/v1/receivers":
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
	case http.MethodPost + "/api/alertmanager/grafana/config/api/v1/receivers/test":
		fallback = middleware.ReqEditorRole
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsWrite)

	// External Alertmanager Paths
	case http.MethodDelete + "/api/alertmanager/{DatasourceUID}/config/api/v1/alerts":
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodGet + "/api/alertmanager/{DatasourceUID}/api/v2/status":
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodGet + "/api/alertmanager/{DatasourceUID}/config/api/v1/alerts":
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))
	case http.MethodPost + "/api/alertmanager/{DatasourceUID}/config/api/v1/alerts":
		eval = ac.EvalPermission(ac.ActionAlertingNotificationsExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID")))

	case http.MethodGet + "/api/v1/ngalert":
		// let user with any alerting permission access this API
		eval = ac.EvalAny(
			ac.EvalPermission(ac.ActionAlertingInstanceRead),
			ac.EvalPermission(ac.ActionAlertingInstancesExternalRead),
			ac.EvalPermission(ac.ActionAlertingRuleRead),
			ac.EvalPermission(ac.ActionAlertingRuleExternalRead),
			ac.EvalPermission(ac.ActionAlertingNotificationsRead),
			ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead),
		)
	// Raw Alertmanager Config Paths
	case http.MethodDelete + "/api/v1/ngalert/admin_config",
		http.MethodGet + "/api/v1/ngalert/admin_config",
		http.MethodPost + "/api/v1/ngalert/admin_config",
		http.MethodGet + "/api/v1/ngalert/alertmanagers":
		return middleware.ReqOrgAdmin

	// Grafana-only Provisioning Read Paths
	case http.MethodGet + "/api/v1/provisioning/policies",
		http.MethodGet + "/api/v1/provisioning/contact-points",
		http.MethodGet + "/api/v1/provisioning/templates",
		http.MethodGet + "/api/v1/provisioning/templates/{name}",
		http.MethodGet + "/api/v1/provisioning/mute-timings",
		http.MethodGet + "/api/v1/provisioning/mute-timings/{name}",
		http.MethodGet + "/api/v1/provisioning/alert-rules",
		http.MethodGet + "/api/v1/provisioning/alert-rules/{UID}",
		http.MethodGet + "/api/v1/provisioning/alert-rules/export",
		http.MethodGet + "/api/v1/provisioning/alert-rules/{UID}/export",
		http.MethodGet + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}",
		http.MethodGet + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export":
		fallback = middleware.ReqOrgAdmin
		eval = ac.EvalPermission(ac.ActionAlertingProvisioningRead) // organization scope

	case http.MethodPut + "/api/v1/provisioning/policies",
		http.MethodDelete + "/api/v1/provisioning/policies",
		http.MethodPost + "/api/v1/provisioning/contact-points",
		http.MethodPut + "/api/v1/provisioning/contact-points/{UID}",
		http.MethodDelete + "/api/v1/provisioning/contact-points/{UID}",
		http.MethodPut + "/api/v1/provisioning/templates/{name}",
		http.MethodDelete + "/api/v1/provisioning/templates/{name}",
		http.MethodPost + "/api/v1/provisioning/mute-timings",
		http.MethodPut + "/api/v1/provisioning/mute-timings/{name}",
		http.MethodDelete + "/api/v1/provisioning/mute-timings/{name}",
		http.MethodPost + "/api/v1/provisioning/alert-rules",
		http.MethodPut + "/api/v1/provisioning/alert-rules/{UID}",
		http.MethodDelete + "/api/v1/provisioning/alert-rules/{UID}",
		http.MethodPut + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}":
		fallback = middleware.ReqOrgAdmin
		eval = ac.EvalPermission(ac.ActionAlertingProvisioningWrite) // organization scope
	}

	if eval != nil {
		return authorize(fallback, eval)
	}

	panic(fmt.Sprintf("no authorization handler for method [%s] of endpoint [%s]", method, path))
}

--- a/pkg/services/ngalert/api/authorization.go
+++ b/pkg/services/ngalert/api/authorization.go
@@ -148,7 +148,7 @@
 		eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
 	case http.MethodPost + "/api/alertmanager/grafana/config/api/v1/receivers/test":
 		fallback = middleware.ReqEditorRole
-		eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
+		eval = ac.EvalPermission(ac.ActionAlertingNotificationsWrite)

 	// External Alertmanager Paths
 	case http.MethodDelete + "/api/alertmanager/{DatasourceUID}/config/api/v1/alerts":


Improper Encoding or Escaping of Output in GitHub repository nilsteampassnet/teampass prior to 3.0.10.

Improper Encoding or Escaping of Output

The product prepares a structured message for communication with another component, but encoding or escaping of the data is either missing or done incorrectly. As a result, the intended structure of the message is not preserved.

function simplePurifier(
    text,
    bHtml = false,
    bSvg = false,
    bSvgFilters = false
)
{
    return DOMPurify.sanitize(
        sanitizeDom(text)
            .replaceAll('<', '<')
            .replaceAll('<', '<')
            .replaceAll('<', '<')
            .replaceAll('>', '>')
            .replaceAll('>', '>')
            .replaceAll('>', '>')
            .replaceAll('&', '&')
            .replaceAll('&', '&')
            .replaceAll('&', '&')
            .replaceAll('"', '"')
            .replaceAll('";', '"')
            .replaceAll('"', '"')
            .replaceAll(''', "'"),
        {USE_PROFILES: {html:bHtml, svg:bSvg, svgFilters: bSvgFilters}}
    );

function simplePurifier(
    text,
    bHtml = false,
    bSvg = false,
    bSvgFilters = false
)
{
    return sanitizeDom(
        DOMPurify.sanitize(
            text
                .replaceAll('<', '<')
                .replaceAll('<', '<')
                .replaceAll('<', '<')
                .replaceAll('<', '<')
                .replaceAll('>', '>')
                .replaceAll('>', '>')
                .replaceAll('>', '>')
                .replaceAll('>', '>')
                .replaceAll('&', '&')
                .replaceAll('&', '&')
                .replaceAll('&', '&')
                .replaceAll('"', '"')
                .replaceAll('";', '"')
                .replaceAll('"', '"')
                .replaceAll(''', "'"),
            {USE_PROFILES: {html:bHtml, svg:bSvg, svgFilters: bSvgFilters}}
        )
    );

--- a/includes/js/functions.js
+++ b/includes/js/functions.js
@@ -5,20 +5,24 @@
     bSvgFilters = false
 )
 {
-    return DOMPurify.sanitize(
-        sanitizeDom(text)
-            .replaceAll('<', '<')
-            .replaceAll('<', '<')
-            .replaceAll('<', '<')
-            .replaceAll('>', '>')
-            .replaceAll('>', '>')
-            .replaceAll('>', '>')
-            .replaceAll('&', '&')
-            .replaceAll('&', '&')
-            .replaceAll('&', '&')
-            .replaceAll('"', '"')
-            .replaceAll('";', '"')
-            .replaceAll('"', '"')
-            .replaceAll(''', "'"),
-        {USE_PROFILES: {html:bHtml, svg:bSvg, svgFilters: bSvgFilters}}
+    return sanitizeDom(
+        DOMPurify.sanitize(
+            text
+                .replaceAll('<', '<')
+                .replaceAll('<', '<')
+                .replaceAll('<', '<')
+                .replaceAll('<', '<')
+                .replaceAll('>', '>')
+                .replaceAll('>', '>')
+                .replaceAll('>', '>')
+                .replaceAll('>', '>')
+                .replaceAll('&', '&')
+                .replaceAll('&', '&')
+                .replaceAll('&', '&')
+                .replaceAll('"', '"')
+                .replaceAll('";', '"')
+                .replaceAll('"', '"')
+                .replaceAll(''', "'"),
+            {USE_PROFILES: {html:bHtml, svg:bSvg, svgFilters: bSvgFilters}}
+        )
     );


Command injection in IrGraph.draw in paddlepaddle/paddle 2.6.0

Improper Control of Generation of Code ('Code Injection')

The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.

Improper Neutralization of Special Elements used in a Command ('Command Injection')

The product constructs all or part of a command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended command when it is sent to a downstream component.

Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.

        def _convert_to_pdf(dot_file_path):
            pdf_save_path = os.path.splitext(dot_file_path)[0] + '.pdf'
            exited_code = subprocess.call(
                'dot -Tpdf ' + dot_file_path + ' -o ' + pdf_save_path,
                shell=True,
            )
            if exited_code != 0:
                print('The dot command is needed for creating pdf files.')
                print(f'The {dot_file_path} is saved as the dot filetype.')

        def _convert_to_pdf(dot_file_path):
            pdf_save_path = os.path.splitext(dot_file_path)[0] + '.pdf'
            exited_code = subprocess.call(
                ['dot', '-Tpdf', dot_file_path, '-o', pdf_save_path]
            )
            if exited_code != 0:
                print('The dot command is needed for creating pdf files.')
                print(f'The {dot_file_path} is saved as the dot filetype.')

--- a/python/paddle/base/framework.py
+++ b/python/paddle/base/framework.py
@@ -1,8 +1,7 @@
         def _convert_to_pdf(dot_file_path):
             pdf_save_path = os.path.splitext(dot_file_path)[0] + '.pdf'
             exited_code = subprocess.call(
-                'dot -Tpdf ' + dot_file_path + ' -o ' + pdf_save_path,
-                shell=True,
+                ['dot', '-Tpdf', dot_file_path, '-o', pdf_save_path]
             )
             if exited_code != 0:
                 print('The dot command is needed for creating pdf files.')


Sequelize, all versions prior to version 4.44.3 and 5.15.1, is vulnerable to SQL Injection due to sequelize.json() helper function not escaping values properly when formatting sub paths for JSON queries for MySQL, MariaDB and SQLite.

Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')

The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data.

  jsonPathExtractionQuery(column, path) {
    let paths = _.toPath(path);
    let pathStr;
    const quotedColumn = this.isIdentifierQuoted(column)
      ? column
      : this.quoteIdentifier(column);

    switch (this.dialect) {
      case 'mysql':
        /**
         * Sub paths need to be quoted as ECMAScript identifiers
         * https://bugs.mysql.com/bug.php?id=81896
         */
        paths = paths.map(subPath => Utils.addTicks(subPath, '"'));
        pathStr = this.escape(['$'].concat(paths).join('.'));
        return `(${quotedColumn}->>${pathStr})`;

      case 'mariadb':
        pathStr = this.escape(['$'].concat(paths).join('.'));
        return `json_unquote(json_extract(${quotedColumn},${pathStr}))`;

      case 'sqlite':
        pathStr = this.escape(['$']
          .concat(paths)
          .join('.')
          .replace(/\.(\d+)(?:(?=\.)|$)/g, (_, digit) => `[${digit}]`));
        return `json_extract(${quotedColumn}, ${pathStr})`;

      case 'postgres':
        pathStr = this.escape(`{${paths.join(',')}}`);
        return `(${quotedColumn}#>>${pathStr})`;

      default:
        throw new Error(`Unsupported ${this.dialect} for JSON operations`);
    }
  }

  handleSequelizeMethod(smth, tableName, factory, options, prepend) {
    if (smth instanceof Utils.Json) {
      // Parse nested object
      if (smth.conditions) {
        const conditions = this.parseConditionObject(smth.conditions).map(condition =>
          `${this.quoteIdentifier(condition.path[0])}->>'$.${_.tail(condition.path).join('.')}' = '${condition.value}'`
        );

        return conditions.join(' and ');
      }
      if (smth.path) {
        let str;

        // Allow specifying conditions using the sqlite json functions
        if (this._checkValidJsonStatement(smth.path)) {
          str = smth.path;
        } else {
          // Also support json dot notation
          let path = smth.path;
          let startWithDot = true;

          // Convert .number. to [number].
          path = path.replace(/\.(\d+)\./g, '[$1].');
          // Convert .number$ to [number]
          path = path.replace(/\.(\d+)$/, '[$1]');

          path = path.split('.');

          let columnName = path.shift();
          const match = columnName.match(/\[\d+\]$/);
          // If columnName ends with [\d+]
          if (match !== null) {
            path.unshift(columnName.substr(match.index));
            columnName = columnName.substr(0, match.index);
            startWithDot = false;
          }

          str = `${this.quoteIdentifier(columnName)}->>'$${startWithDot ? '.' : ''}${path.join('.')}'`;
        }

        if (smth.value) {
          str += util.format(' = %s', this.escape(smth.value));
        }

        return str;
      }
    } else if (smth instanceof Utils.Cast) {
      if (/timestamp/i.test(smth.type)) {
        smth.type = 'datetime';
      } else if (smth.json && /boolean/i.test(smth.type)) {
        // true or false cannot be casted as booleans within a JSON structure
        smth.type = 'char';
      } else if (/double precision/i.test(smth.type) || /boolean/i.test(smth.type) || /integer/i.test(smth.type)) {
        smth.type = 'decimal';
      } else if (/text/i.test(smth.type)) {
        smth.type = 'char';
      }
    }

    return super.handleSequelizeMethod(smth, tableName, factory, options, prepend);
  }

  handleSequelizeMethod(smth, tableName, factory, options, prepend) {
    if (smth instanceof Utils.Json) {
      // Parse nested object
      if (smth.conditions) {
        const conditions = this.parseConditionObject(smth.conditions).map(condition =>
          `${this.jsonPathExtractionQuery(condition.path[0], _.tail(condition.path))} = '${condition.value}'`
        );

        return conditions.join(' AND ');
      }
      if (smth.path) {
        let str;

        // Allow specifying conditions using the sqlite json functions
        if (this._checkValidJsonStatement(smth.path)) {
          str = smth.path;
        } else {
          // Also support json property accessors
          const paths = _.toPath(smth.path);
          const column = paths.shift();
          str = this.jsonPathExtractionQuery(column, paths);
        }

        if (smth.value) {
          str += util.format(' = %s', this.escape(smth.value));
        }

        return str;
      }
    } else if (smth instanceof Utils.Cast) {
      if (/timestamp/i.test(smth.type)) {
        smth.type = 'datetime';
      }
    }

    return AbstractQueryGenerator.prototype.handleSequelizeMethod.call(this, smth, tableName, factory, options, prepend);
  }

  handleSequelizeMethod(smth, tableName, factory, options, prepend) {
    if (smth instanceof Utils.Json) {
      // Parse nested object
      if (smth.conditions) {
        const conditions = this.parseConditionObject(smth.conditions).map(
          condition =>
            `json_unquote(json_extract(${this.quoteIdentifier(
              condition.path[0])},'$.${_.tail(condition.path).join(
              '.')}')) = '${condition.value}'`
        );

        return conditions.join(' and ');
      }
      if (smth.path) {
        let str;

        // Allow specifying conditions using the sqlite json functions
        if (this._checkValidJsonStatement(smth.path)) {
          str = smth.path;
        } else {
          // Also support json dot notation
          let path = smth.path;
          let startWithDot = true;

          // Convert .number. to [number].
          path = path.replace(/\.(\d+)\./g, '[$1].');
          // Convert .number$ to [number]
          path = path.replace(/\.(\d+)$/, '[$1]');

          path = path.split('.');

          let columnName = path.shift();
          const match = columnName.match(/\[\d+\]$/);
          // If columnName ends with [\d+]
          if (match !== null) {
            path.unshift(columnName.substr(match.index));
            columnName = columnName.substr(0, match.index);
            startWithDot = false;
          }

          str = `json_unquote(json_extract(${this.quoteIdentifier(
            columnName)},'$${startWithDot ? '.' : ''}${path.join('.')}'))`;
        }

        if (smth.value) {
          str += util.format(' = %s', this.escape(smth.value));
        }

        return str;
      }
    } else if (smth instanceof Utils.Cast) {
      const lowType = smth.type.toLowerCase();
      if (lowType.includes('timestamp')) {
        smth.type = 'datetime';
      } else if (smth.json && lowType.includes('boolean')) {
        // true or false cannot be casted as booleans within a JSON structure
        smth.type = 'char';
      } else if (lowType.includes('double precision') || lowType.includes('boolean') || lowType.includes('integer')) {
        smth.type = 'decimal';
      } else if (lowType.includes('text')) {
        smth.type = 'char';
      }
    }

    return super.handleSequelizeMethod(smth, tableName, factory, options, prepend);
  }

  jsonPathExtractionQuery(column, path) {
    let paths = _.toPath(path);
    let pathStr;
    const quotedColumn = this.isIdentifierQuoted(column)
      ? column
      : this.quoteIdentifier(column);

    switch (this.dialect) {
      case 'mysql':
      case 'mariadb':
      case 'sqlite':
        /**
         * Non digit sub paths need to be quoted as ECMAScript identifiers
         * https://bugs.mysql.com/bug.php?id=81896
         */
        if (this.dialect === 'mysql') {
          paths = paths.map(subPath => {
            return /\D/.test(subPath)
              ? Utils.addTicks(subPath, '"')
              : subPath;
          });
        }

        pathStr = this.escape(['$']
          .concat(paths)
          .join('.')
          .replace(/\.(\d+)(?:(?=\.)|$)/g, (__, digit) => `[${digit}]`));

        if (this.dialect === 'sqlite') {
          return `json_extract(${quotedColumn},${pathStr})`;
        }

        return `json_unquote(json_extract(${quotedColumn},${pathStr}))`;

      case 'postgres':
        pathStr = this.escape(`{${paths.join(',')}}`);
        return `(${quotedColumn}#>>${pathStr})`;

      default:
        throw new Error(`Unsupported ${this.dialect} for JSON operations`);
    }
  }

  handleSequelizeMethod(smth, tableName, factory, options, prepend) {
    if (smth instanceof Utils.Json) {
      // Parse nested object
      if (smth.conditions) {
        const conditions = this.parseConditionObject(smth.conditions).map(condition =>
          `${this.jsonPathExtractionQuery(condition.path[0], _.tail(condition.path))} = '${condition.value}'`
        );

        return conditions.join(' AND ');
      }
      if (smth.path) {
        let str;

        // Allow specifying conditions using the sqlite json functions
        if (this._checkValidJsonStatement(smth.path)) {
          str = smth.path;
        } else {
          // Also support json property accessors
          const paths = _.toPath(smth.path);
          const column = paths.shift();
          str = this.jsonPathExtractionQuery(column, paths);
        }

        if (smth.value) {
          str += util.format(' = %s', this.escape(smth.value));
        }

        return str;
      }
    } else if (smth instanceof Utils.Cast) {
      if (/timestamp/i.test(smth.type)) {
        smth.type = 'datetime';
      } else if (smth.json && /boolean/i.test(smth.type)) {
        // true or false cannot be casted as booleans within a JSON structure
        smth.type = 'char';
      } else if (/double precision/i.test(smth.type) || /boolean/i.test(smth.type) || /integer/i.test(smth.type)) {
        smth.type = 'decimal';
      } else if (/text/i.test(smth.type)) {
        smth.type = 'char';
      }
    }

    return super.handleSequelizeMethod(smth, tableName, factory, options, prepend);
  }

  handleSequelizeMethod(smth, tableName, factory, options, prepend) {
    if (smth instanceof Utils.Json) {
      return super.handleSequelizeMethod(smth, tableName, factory, options, prepend);
    }

    if (smth instanceof Utils.Cast) {
      if (/timestamp/i.test(smth.type)) {
        smth.type = 'datetime';
      }
    }

    return AbstractQueryGenerator.prototype.handleSequelizeMethod.call(this, smth, tableName, factory, options, prepend);
  }

--- a/lib/dialects/abstract/query-generator.js
+++ b/lib/dialects/abstract/query-generator.js
@@ -7,24 +7,30 @@

     switch (this.dialect) {
       case 'mysql':
+      case 'mariadb':
+      case 'sqlite':
         /**
-         * Sub paths need to be quoted as ECMAScript identifiers
+         * Non digit sub paths need to be quoted as ECMAScript identifiers
          * https://bugs.mysql.com/bug.php?id=81896
          */
-        paths = paths.map(subPath => Utils.addTicks(subPath, '"'));
-        pathStr = this.escape(['$'].concat(paths).join('.'));
-        return `(${quotedColumn}->>${pathStr})`;
+        if (this.dialect === 'mysql') {
+          paths = paths.map(subPath => {
+            return /\D/.test(subPath)
+              ? Utils.addTicks(subPath, '"')
+              : subPath;
+          });
+        }

-      case 'mariadb':
-        pathStr = this.escape(['$'].concat(paths).join('.'));
-        return `json_unquote(json_extract(${quotedColumn},${pathStr}))`;
-
-      case 'sqlite':
         pathStr = this.escape(['$']
           .concat(paths)
           .join('.')
-          .replace(/\.(\d+)(?:(?=\.)|$)/g, (_, digit) => `[${digit}]`));
-        return `json_extract(${quotedColumn}, ${pathStr})`;
+          .replace(/\.(\d+)(?:(?=\.)|$)/g, (__, digit) => `[${digit}]`));
+
+        if (this.dialect === 'sqlite') {
+          return `json_extract(${quotedColumn},${pathStr})`;
+        }
+
+        return `json_unquote(json_extract(${quotedColumn},${pathStr}))`;

       case 'postgres':
         pathStr = this.escape(`{${paths.join(',')}}`);

--- a/lib/dialects/mysql/query-generator.js
+++ b/lib/dialects/mysql/query-generator.js
@@ -3,10 +3,10 @@
       // Parse nested object
       if (smth.conditions) {
         const conditions = this.parseConditionObject(smth.conditions).map(condition =>
-          `${this.quoteIdentifier(condition.path[0])}->>'$.${_.tail(condition.path).join('.')}' = '${condition.value}'`
+          `${this.jsonPathExtractionQuery(condition.path[0], _.tail(condition.path))} = '${condition.value}'`
         );

-        return conditions.join(' and ');
+        return conditions.join(' AND ');
       }
       if (smth.path) {
         let str;
@@ -15,27 +15,10 @@
         if (this._checkValidJsonStatement(smth.path)) {
           str = smth.path;
         } else {
-          // Also support json dot notation
-          let path = smth.path;
-          let startWithDot = true;
-
-          // Convert .number. to [number].
-          path = path.replace(/\.(\d+)\./g, '[$1].');
-          // Convert .number$ to [number]
-          path = path.replace(/\.(\d+)$/, '[$1]');
-
-          path = path.split('.');
-
-          let columnName = path.shift();
-          const match = columnName.match(/\[\d+\]$/);
-          // If columnName ends with [\d+]
-          if (match !== null) {
-            path.unshift(columnName.substr(match.index));
-            columnName = columnName.substr(0, match.index);
-            startWithDot = false;
-          }
-
-          str = `${this.quoteIdentifier(columnName)}->>'$${startWithDot ? '.' : ''}${path.join('.')}'`;
+          // Also support json property accessors
+          const paths = _.toPath(smth.path);
+          const column = paths.shift();
+          str = this.jsonPathExtractionQuery(column, paths);
         }

         if (smth.value) {

--- a/lib/dialects/sqlite/query-generator.js
+++ b/lib/dialects/sqlite/query-generator.js
@@ -1,33 +1,9 @@
   handleSequelizeMethod(smth, tableName, factory, options, prepend) {
     if (smth instanceof Utils.Json) {
-      // Parse nested object
-      if (smth.conditions) {
-        const conditions = this.parseConditionObject(smth.conditions).map(condition =>
-          `${this.jsonPathExtractionQuery(condition.path[0], _.tail(condition.path))} = '${condition.value}'`
-        );
+      return super.handleSequelizeMethod(smth, tableName, factory, options, prepend);
+    }

-        return conditions.join(' AND ');
-      }
-      if (smth.path) {
-        let str;
-
-        // Allow specifying conditions using the sqlite json functions
-        if (this._checkValidJsonStatement(smth.path)) {
-          str = smth.path;
-        } else {
-          // Also support json property accessors
-          const paths = _.toPath(smth.path);
-          const column = paths.shift();
-          str = this.jsonPathExtractionQuery(column, paths);
-        }
-
-        if (smth.value) {
-          str += util.format(' = %s', this.escape(smth.value));
-        }
-
-        return str;
-      }
-    } else if (smth instanceof Utils.Cast) {
+    if (smth instanceof Utils.Cast) {
       if (/timestamp/i.test(smth.type)) {
         smth.type = 'datetime';
       }

--- a/lib/dialects/mariadb/query-generator.js
+++ /dev/null
@@ -1,66 +0,0 @@
-  handleSequelizeMethod(smth, tableName, factory, options, prepend) {
-    if (smth instanceof Utils.Json) {
-      // Parse nested object
-      if (smth.conditions) {
-        const conditions = this.parseConditionObject(smth.conditions).map(
-          condition =>
-            `json_unquote(json_extract(${this.quoteIdentifier(
-              condition.path[0])},'$.${_.tail(condition.path).join(
-              '.')}')) = '${condition.value}'`
-        );
-
-        return conditions.join(' and ');
-      }
-      if (smth.path) {
-        let str;
-
-        // Allow specifying conditions using the sqlite json functions
-        if (this._checkValidJsonStatement(smth.path)) {
-          str = smth.path;
-        } else {
-          // Also support json dot notation
-          let path = smth.path;
-          let startWithDot = true;
-
-          // Convert .number. to [number].
-          path = path.replace(/\.(\d+)\./g, '[$1].');
-          // Convert .number$ to [number]
-          path = path.replace(/\.(\d+)$/, '[$1]');
-
-          path = path.split('.');
-
-          let columnName = path.shift();
-          const match = columnName.match(/\[\d+\]$/);
-          // If columnName ends with [\d+]
-          if (match !== null) {
-            path.unshift(columnName.substr(match.index));
-            columnName = columnName.substr(0, match.index);
-            startWithDot = false;
-          }
-
-          str = `json_unquote(json_extract(${this.quoteIdentifier(
-            columnName)},'$${startWithDot ? '.' : ''}${path.join('.')}'))`;
-        }
-
-        if (smth.value) {
-          str += util.format(' = %s', this.escape(smth.value));
-        }
-
-        return str;
-      }
-    } else if (smth instanceof Utils.Cast) {
-      const lowType = smth.type.toLowerCase();
-      if (lowType.includes('timestamp')) {
-        smth.type = 'datetime';
-      } else if (smth.json && lowType.includes('boolean')) {
-        // true or false cannot be casted as booleans within a JSON structure
-        smth.type = 'char';
-      } else if (lowType.includes('double precision') || lowType.includes('boolean') || lowType.includes('integer')) {
-        smth.type = 'decimal';
-      } else if (lowType.includes('text')) {
-        smth.type = 'char';
-      }
-    }
-
-    return super.handleSequelizeMethod(smth, tableName, factory, options, prepend);
-  }


workers/extractor.py in Pandora (aka pandora-analysis/pandora) 1.3.0 allows a denial of service when an attacker submits a deeply nested ZIP archive (aka ZIP bomb).

Improper Input Validation

The product receives input or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.

    def analyse(self, task: Task, report: Report, manual_trigger: bool=False):
        # The files supported by dfvfs generally don't have proper mime types, so we just try it on everything.
        dfvfs_info = self.check_dfvfs(task.file, True)
        if not (task.file.is_archive or task.file.is_eml or task.file.is_msg or dfvfs_info):
            report.status = Status.NOTAPPLICABLE
            return

        if not task.user:
            raise PandoraException('The task user is missing. Should not happen, but investigate if it does.')

        pandora = Pandora()

        tasks: List[Task] = []
        extracted_dir = task.file.directory / 'extracted'
        safe_create_dir(extracted_dir)
        extracted: Sequence[Union[Path, Tuple[str, BytesIO]]] = []

        # Try to extract files from archive
        # TODO: Support other archive formats
        if task.file.is_archive:
            if task.password:
                self.passwords = [task.password]
            else:
                self.passwords = self.zip_passwords
            try:
                if task.file.mime_type == "application/x-7z-compressed":
                    extracted = self._extract_7z(task.file, report, extracted_dir)
                elif task.file.mime_type == "application/vnd.ms-cab-compressed":
                    extracted = self._extract_cab(task.file, report, extracted_dir)
                elif task.file.mime_type == "application/x-rar":
                    extracted = self._extract_rar(task.file, report, extracted_dir)
                elif task.file.mime_type == "application/x-bzip2":
                    extracted = self._extract_bz2(task.file, report, extracted_dir)
                elif task.file.mime_type == "application/gzip":
                    extracted = self._extract_gz(task.file, report, extracted_dir)
                elif task.file.mime_type == "application/x-tar":
                    extracted = self._extract_tar(task.file, report, extracted_dir)
                elif task.file.mime_type in ["application/x-lzma", "application/x-xz", "application/x-lzip"]:
                    extracted = self._extract_lzma(task.file, report, extracted_dir)
                elif task.file.mime_type == "application/x-iso9660-image":
                    extracted = self._extract_iso(task.file, report, extracted_dir)
                elif task.file.mime_type == "application/zip":
                    extracted = self._extract_zip(task.file, report, extracted_dir)
                    if not extracted:
                        report.clear_extras()
                        report.clear_details()
                        report.reset_status()
                        extracted = self._extract_zip(task.file, report, extracted_dir, pyzipper.AESZipFile)
                else:
                    raise PandoraException(f'Unsupported mimetype: {task.file.mime_type}')
            except BaseException as e:
                report.status = Status.WARN
                report.add_details('Warning', f'Unable to extract {task.file.path.name}: {e}.')
                report.add_extra('no_password', True)
                extracted = []
                self.logger.exception(e)

        # Try to extract attachments from EML file
        if task.file.is_eml:
            if not task.file.eml_data or 'attachment' not in task.file.eml_data or not task.file.eml_data['attachment']:
                report.status = Status.NOTAPPLICABLE
            else:
                try:
                    extracted = self.extract_eml(task.file.eml_data)
                except Exception as e:
                    self.logger.exception(e)

        elif task.file.is_msg:
            if not task.file.msg_data or not task.file.msg_data.attachments:
                report.status = Status.NOTAPPLICABLE
            else:
                try:
                    extracted = self.extract_msg(task.file.msg_data)
                except Exception as e:
                    self.logger.exception(e)

        elif dfvfs_info:
            # this is a dfvfs supported file
            try:
                extracted = self.extract_with_dfvfs(task.file, report)
            except Exception as e:
                self.logger.exception('dfVFS dislikes it.')
                report.status = Status.WARN
                report.add_details('Warning', f'Unable to process with dfVFS {task.file.path.name}: {e}.')

        if not extracted and report.status != Status.NOTAPPLICABLE:
            report.status = Status.WARN
            report.add_details('Warning', 'Nothing to extract.')

        for ef in extracted:
            if isinstance(ef, Path):
                filename = ef.name
                with ef.open('rb') as f:
                    sample = BytesIO(f.read())
            else:
                filename, sample = ef
            new_task = Task.new_task(user=task.user, sample=sample,
                                     filename=filename,
                                     disabled_workers=task.disabled_workers,
                                     parent=task)
            pandora.add_extracted_reference(task, new_task)
            pandora.enqueue_task(new_task)
            tasks.append(new_task)

        shutil.rmtree(extracted_dir)
        # wait for all the tasks to finish
        while not all(t.workers_done for t in tasks):
            time.sleep(1)

        if tasks:
            report.status = max(t.status for t in tasks)
            if report.status > Status.CLEAN:
                report.add_details('Warning', 'There are suspicious files in this archive, click on the "Extracted" tab for more.')
        elif not report.status == Status.NOTAPPLICABLE:
            # Nothing was extracted
            report.status = Status.WARN
            report.add_details('Warning', 'Looks like the archive is empty (?). This is suspicious.')

    def analyse(self, task: Task, report: Report, manual_trigger: bool=False):
        # The files supported by dfvfs generally don't have proper mime types, so we just try it on everything.
        dfvfs_info = self.check_dfvfs(task.file, True)
        if not (task.file.is_archive or task.file.is_eml or task.file.is_msg or dfvfs_info):
            report.status = Status.NOTAPPLICABLE
            return

        # Check if we reach the max recursivity (archive in archive in archive...)
        _curtask = task
        _cur_recurse = self.max_recurse
        _cur_max_files_in_recurse = self.max_files_in_recursive_archive
        while _cur_recurse > 0 and _cur_max_files_in_recurse > 0:
            if not _curtask.parent:
                break
            _curtask = _curtask.parent
            _cur_recurse -= 1
            _cur_max_files_in_recurse -= len(_curtask.extracted)

        if _cur_recurse <= 0:
            self.logger.warning(f'File {task.file.path.name} is too deep in the recursion chain (>{self.max_recurse}).')
            report.status = Status.ERROR if self.max_is_error else Status.ALERT
            report.add_details('Warning', f'File {task.file.path.name} is too deep in the recursion chain (>{self.max_recurse}). If you want to scan it anyway, click on Actions > Rescan file.')
            return

        if _cur_max_files_in_recurse <= 0:
            self.logger.warning(f'File {task.file.path.name} cannot be extracted, too many files (>{self.max_files_in_recursive_archive}) in the recursive archive.')
            report.status = Status.ERROR if self.max_is_error else Status.ALERT
            report.add_details('Warning', f'File {task.file.path.name} cannot be extracted, too many files (>{self.max_files_in_recursive_archive}) in the recursive archive. If you want to scan it anyway, click on Actions > Rescan file.')
            return

        if not task.user:
            raise PandoraException('The task user is missing. Should not happen, but investigate if it does.')

        pandora = Pandora()

        tasks: List[Task] = []
        extracted_dir = task.file.directory / 'extracted'
        safe_create_dir(extracted_dir)
        extracted: Sequence[Union[Path, Tuple[str, BytesIO]]] = []

        # Try to extract files from archive
        # TODO: Support other archive formats
        if task.file.is_archive:
            if task.password:
                self.passwords = [task.password]
            else:
                self.passwords = self.zip_passwords
            try:
                if task.file.mime_type == "application/x-7z-compressed":
                    extracted = self._extract_7z(task.file, report, extracted_dir)
                elif task.file.mime_type == "application/vnd.ms-cab-compressed":
                    extracted = self._extract_cab(task.file, report, extracted_dir)
                elif task.file.mime_type == "application/x-rar":
                    extracted = self._extract_rar(task.file, report, extracted_dir)
                elif task.file.mime_type == "application/x-bzip2":
                    extracted = self._extract_bz2(task.file, report, extracted_dir)
                elif task.file.mime_type == "application/gzip":
                    extracted = self._extract_gz(task.file, report, extracted_dir)
                elif task.file.mime_type == "application/x-tar":
                    extracted = self._extract_tar(task.file, report, extracted_dir)
                elif task.file.mime_type in ["application/x-lzma", "application/x-xz", "application/x-lzip"]:
                    extracted = self._extract_lzma(task.file, report, extracted_dir)
                elif task.file.mime_type == "application/x-iso9660-image":
                    extracted = self._extract_iso(task.file, report, extracted_dir)
                elif task.file.mime_type == "application/zip":
                    extracted = self._extract_zip(task.file, report, extracted_dir)
                    if not extracted:
                        report.clear_extras()
                        report.clear_details()
                        report.reset_status()
                        extracted = self._extract_zip(task.file, report, extracted_dir, pyzipper.AESZipFile)
                else:
                    raise PandoraException(f'Unsupported mimetype: {task.file.mime_type}')
            except BaseException as e:
                report.status = Status.WARN
                report.add_details('Warning', f'Unable to extract {task.file.path.name}: {e}.')
                report.add_extra('no_password', True)
                extracted = []
                self.logger.exception(e)

        # Try to extract attachments from EML file
        if task.file.is_eml:
            if not task.file.eml_data or 'attachment' not in task.file.eml_data or not task.file.eml_data['attachment']:
                report.status = Status.NOTAPPLICABLE
            else:
                try:
                    extracted = self.extract_eml(task.file.eml_data)
                except Exception as e:
                    self.logger.exception(e)

        elif task.file.is_msg:
            if not task.file.msg_data or not task.file.msg_data.attachments:
                report.status = Status.NOTAPPLICABLE
            else:
                try:
                    extracted = self.extract_msg(task.file.msg_data)
                except Exception as e:
                    self.logger.exception(e)

        elif dfvfs_info:
            # this is a dfvfs supported file
            try:
                extracted = self.extract_with_dfvfs(task.file, report)
            except Exception as e:
                self.logger.exception('dfVFS dislikes it.')
                report.status = Status.WARN
                report.add_details('Warning', f'Unable to process with dfVFS {task.file.path.name}: {e}.')

        if not extracted and report.status != Status.NOTAPPLICABLE:
            report.status = Status.WARN
            report.add_details('Warning', 'Nothing to extract.')

        for ef in extracted:
            if isinstance(ef, Path):
                filename = ef.name
                with ef.open('rb') as f:
                    sample = BytesIO(f.read())
            else:
                filename, sample = ef
            new_task = Task.new_task(user=task.user, sample=sample,
                                     filename=filename,
                                     disabled_workers=task.disabled_workers,
                                     parent=task)
            pandora.add_extracted_reference(task, new_task)
            pandora.enqueue_task(new_task)
            tasks.append(new_task)

        shutil.rmtree(extracted_dir)

        if not tasks and not report.status == Status.NOTAPPLICABLE:
            # Nothing was extracted
            report.status = Status.WARN
            report.add_details('Warning', 'Looks like the archive is empty (?). This is suspicious.')
        elif report.status not in [Status.ERROR, Status.WARN, Status.ALERT, Status.OVERWRITE]:
            # wait for all the workers to finish, or have one of them raising an ALERT
            while not all(t.workers_done for t in tasks):
                for t in tasks:
                    # If any of the task is marked as ALERT or OVERWRITE, we can quit.
                    if t.workers_done and t.status >= Status.ALERT:
                        report.add_details('Warning', 'There are suspicious files in this archive, click on the "Extracted" tab for more.')
                        break
                time.sleep(1)
            report.status = max(t.status for t in tasks if t.workers_done)

--- a/pandora/workers/extractor.py
+++ b/pandora/workers/extractor.py
@@ -3,6 +3,29 @@
         dfvfs_info = self.check_dfvfs(task.file, True)
         if not (task.file.is_archive or task.file.is_eml or task.file.is_msg or dfvfs_info):
             report.status = Status.NOTAPPLICABLE
+            return
+
+        # Check if we reach the max recursivity (archive in archive in archive...)
+        _curtask = task
+        _cur_recurse = self.max_recurse
+        _cur_max_files_in_recurse = self.max_files_in_recursive_archive
+        while _cur_recurse > 0 and _cur_max_files_in_recurse > 0:
+            if not _curtask.parent:
+                break
+            _curtask = _curtask.parent
+            _cur_recurse -= 1
+            _cur_max_files_in_recurse -= len(_curtask.extracted)
+
+        if _cur_recurse <= 0:
+            self.logger.warning(f'File {task.file.path.name} is too deep in the recursion chain (>{self.max_recurse}).')
+            report.status = Status.ERROR if self.max_is_error else Status.ALERT
+            report.add_details('Warning', f'File {task.file.path.name} is too deep in the recursion chain (>{self.max_recurse}). If you want to scan it anyway, click on Actions > Rescan file.')
+            return
+
+        if _cur_max_files_in_recurse <= 0:
+            self.logger.warning(f'File {task.file.path.name} cannot be extracted, too many files (>{self.max_files_in_recursive_archive}) in the recursive archive.')
+            report.status = Status.ERROR if self.max_is_error else Status.ALERT
+            report.add_details('Warning', f'File {task.file.path.name} cannot be extracted, too many files (>{self.max_files_in_recursive_archive}) in the recursive archive. If you want to scan it anyway, click on Actions > Rescan file.')
             return

         if not task.user:
@@ -103,15 +126,18 @@
             tasks.append(new_task)

         shutil.rmtree(extracted_dir)
-        # wait for all the tasks to finish
-        while not all(t.workers_done for t in tasks):
-            time.sleep(1)

-        if tasks:
-            report.status = max(t.status for t in tasks)
-            if report.status > Status.CLEAN:
-                report.add_details('Warning', 'There are suspicious files in this archive, click on the "Extracted" tab for more.')
-        elif not report.status == Status.NOTAPPLICABLE:
+        if not tasks and not report.status == Status.NOTAPPLICABLE:
             # Nothing was extracted
             report.status = Status.WARN
             report.add_details('Warning', 'Looks like the archive is empty (?). This is suspicious.')
+        elif report.status not in [Status.ERROR, Status.WARN, Status.ALERT, Status.OVERWRITE]:
+            # wait for all the workers to finish, or have one of them raising an ALERT
+            while not all(t.workers_done for t in tasks):
+                for t in tasks:
+                    # If any of the task is marked as ALERT or OVERWRITE, we can quit.
+                    if t.workers_done and t.status >= Status.ALERT:
+                        report.add_details('Warning', 'There are suspicious files in this archive, click on the "Extracted" tab for more.')
+                        break
+                time.sleep(1)
+            report.status = max(t.status for t in tasks if t.workers_done)


Zitadel is an open source identity management system. Zitadel uses a cookie to identify the user agent (browser) and its user sessions.  Although the cookie was handled according to best practices, it was accessible on subdomains of the ZITADEL instance. An attacker could take advantage of this and provide a malicious link hosted on the subdomain to the user to gain access to the victim’s account in certain scenarios. A possible victim would need to login through the malicious link for this exploit to work. If the possible victim already had the cookie present, the attack would not succeed. The attack would further only be possible if there was an initial vulnerability on the subdomain. This could either be the attacker being able to control DNS or a XSS vulnerability in an application hosted on a subdomain. Versions 2.46.0, 2.45.1, and 2.44.3 have been patched. Zitadel recommends upgrading to the latest versions available in due course. Note that applying the patch will invalidate the current cookie and thus users will need to start a new session and existing sessions (user selection) will be empty. For self-hosted environments unable to upgrade to a patched version, prevent setting the following cookie name on subdomains of your Zitadel instance (e.g. within your WAF): `__Secure-zitadel-useragent`.

Improper Authorization

The product does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action.

Execution with Unnecessary Privileges

The product performs an operation at a privilege level that is higher than the minimum level required, which creates new weaknesses or amplifies the consequences of other weaknesses.

Improper Privilege Management

The product does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.

func (c *CookieHandler) httpSetWithSameSite(w http.ResponseWriter, name, host, value string, maxAge int, sameSite http.SameSite) {
	domain := strings.Split(host, ":")[0]
	// same site none requires the secure flag, so we'll set it even if the cookie is set on non-TLS for localhost
	secure := c.secureOnly || (sameSite == http.SameSiteNoneMode && domain == "localhost")
	// prefix the cookie for secure cookies (TLS only, therefore not for samesite none on http://localhost)
	prefixedName := SetCookiePrefix(name, c.secureOnly, c.prefix)
	http.SetCookie(w, &http.Cookie{
		Name:     prefixedName,
		Value:    value,
		Domain:   domain,
		Path:     c.path,
		MaxAge:   maxAge,
		HttpOnly: c.httpOnly,
		Secure:   secure,
		SameSite: sameSite,
	})
	varyValues := w.Header().Values("vary")
	for _, vary := range varyValues {
		if vary == "Cookie" {
			return
		}
	}
	w.Header().Add("vary", "Cookie")
}

func NewUserAgentHandler(config *UserAgentCookieConfig, cookieKey []byte, idGenerator id.Generator, externalSecure bool, ignoredPrefixes ...string) (func(http.Handler) http.Handler, error) {
	opts := []http_utils.CookieHandlerOpt{
		http_utils.WithEncryption(cookieKey, cookieKey),
		http_utils.WithMaxAge(int(config.MaxAge.Seconds())),
		http_utils.WithPrefix(http_utils.PrefixSecure),
	}
	if !externalSecure {
		opts = append(opts, http_utils.WithUnsecure())
	}
	return func(handler http.Handler) http.Handler {
		return &userAgentHandler{
			nextHandler:     handler,
			cookieName:      config.Name,
			cookieHandler:   http_utils.NewCookieHandler(opts...),
			idGenerator:     idGenerator,
			ignoredPrefixes: ignoredPrefixes,
		}
	}, nil
}

func (c *CookieHandler) httpSetWithSameSite(w http.ResponseWriter, name, host, value string, maxAge int, sameSite http.SameSite) {
	domain := strings.Split(host, ":")[0]
	// same site none requires the secure flag, so we'll set it even if the cookie is set on non-TLS for localhost
	secure := c.secureOnly || (sameSite == http.SameSiteNoneMode && domain == "localhost")
	// prefix the cookie for secure cookies (TLS only, therefore not for samesite none on http://localhost)
	prefixedName := SetCookiePrefix(name, c.secureOnly, c.prefix)
	// in case the host prefix is set, we need to make sure the domain is not set (otherwise the browser will reject the cookie)
	if secure && c.prefix == PrefixHost {
		domain = ""
	}
	http.SetCookie(w, &http.Cookie{
		Name:     prefixedName,
		Value:    value,
		Domain:   domain,
		Path:     c.path,
		MaxAge:   maxAge,
		HttpOnly: c.httpOnly,
		Secure:   secure,
		SameSite: sameSite,
	})
	varyValues := w.Header().Values("vary")
	for _, vary := range varyValues {
		if vary == "Cookie" {
			return
		}
	}
	w.Header().Add("vary", "Cookie")
}

func NewUserAgentHandler(config *UserAgentCookieConfig, cookieKey []byte, idGenerator id.Generator, externalSecure bool, ignoredPrefixes ...string) (func(http.Handler) http.Handler, error) {
	opts := []http_utils.CookieHandlerOpt{
		http_utils.WithEncryption(cookieKey, cookieKey),
		http_utils.WithMaxAge(int(config.MaxAge.Seconds())),
		http_utils.WithPrefix(http_utils.PrefixHost),
	}
	if !externalSecure {
		opts = append(opts, http_utils.WithUnsecure())
	}
	return func(handler http.Handler) http.Handler {
		return &userAgentHandler{
			nextHandler:     handler,
			cookieName:      config.Name,
			cookieHandler:   http_utils.NewCookieHandler(opts...),
			idGenerator:     idGenerator,
			ignoredPrefixes: ignoredPrefixes,
		}
	}, nil
}

--- a/internal/api/http/cookie.go
+++ b/internal/api/http/cookie.go
@@ -4,6 +4,10 @@
 	secure := c.secureOnly || (sameSite == http.SameSiteNoneMode && domain == "localhost")
 	// prefix the cookie for secure cookies (TLS only, therefore not for samesite none on http://localhost)
 	prefixedName := SetCookiePrefix(name, c.secureOnly, c.prefix)
+	// in case the host prefix is set, we need to make sure the domain is not set (otherwise the browser will reject the cookie)
+	if secure && c.prefix == PrefixHost {
+		domain = ""
+	}
 	http.SetCookie(w, &http.Cookie{
 		Name:     prefixedName,
 		Value:    value,

--- a/internal/api/http/middleware/user_agent_cookie.go
+++ b/internal/api/http/middleware/user_agent_cookie.go
@@ -2,7 +2,7 @@
 	opts := []http_utils.CookieHandlerOpt{
 		http_utils.WithEncryption(cookieKey, cookieKey),
 		http_utils.WithMaxAge(int(config.MaxAge.Seconds())),
-		http_utils.WithPrefix(http_utils.PrefixSecure),
+		http_utils.WithPrefix(http_utils.PrefixHost),
 	}
 	if !externalSecure {
 		opts = append(opts, http_utils.WithUnsecure())


fontTools is a library for manipulating fonts, written in Python. The subsetting module has a XML External Entity Injection (XXE) vulnerability which allows an attacker to resolve arbitrary entities when a candidate font (OT-SVG fonts), which contains a SVG table, is parsed. This allows attackers to include arbitrary files from the filesystem fontTools is running on or make web requests from the host system. This vulnerability has been patched in version 4.43.0.

Improper Restriction of XML External Entity Reference

The product processes an XML document that can contain XML entities with URIs that resolve to documents outside of the intended sphere of control, causing the product to embed incorrect documents into its output.

def subset_glyphs(self, s) -> bool:
    if etree is None:
        raise ImportError("No module named 'lxml', required to subset SVG")

    # glyph names (before subsetting)
    glyph_order: List[str] = s.orig_glyph_order
    # map from glyph names to original glyph indices
    rev_orig_glyph_map: Dict[str, int] = s.reverseOrigGlyphMap
    # map from original to new glyph indices (after subsetting)
    glyph_index_map: Dict[int, int] = s.glyph_index_map

    new_docs: List[SVGDocument] = []
    for doc in self.docList:

        glyphs = {
            glyph_order[i] for i in range(doc.startGlyphID, doc.endGlyphID + 1)
        }.intersection(s.glyphs)
        if not glyphs:
            # no intersection: we can drop the whole record
            continue

        svg = etree.fromstring(
            # encode because fromstring dislikes xml encoding decl if input is str.
            # SVG xml encoding must be utf-8 as per OT spec.
            doc.data.encode("utf-8"),
            parser=etree.XMLParser(
                # Disable libxml2 security restrictions to support very deep trees.
                # Without this we would get an error like this:
                # `lxml.etree.XMLSyntaxError: internal error: Huge input lookup`
                # when parsing big fonts e.g. noto-emoji-picosvg.ttf.
                huge_tree=True,
                # ignore blank text as it's not meaningful in OT-SVG; it also prevents
                # dangling tail text after removing an element when pretty_print=True
                remove_blank_text=True,
            ),
        )

        elements = group_elements_by_id(svg)
        gids = {rev_orig_glyph_map[g] for g in glyphs}
        element_ids = {f"glyph{i}" for i in gids}
        closure_element_ids(elements, element_ids)

        if not subset_elements(svg, element_ids):
            continue

        if not s.options.retain_gids:
            id_map = remap_glyph_ids(svg, glyph_index_map)
            update_glyph_href_links(svg, id_map)

        new_doc = etree.tostring(svg, pretty_print=s.options.pretty_svg).decode("utf-8")

        new_gids = (glyph_index_map[i] for i in gids)
        for start, end in ranges(new_gids):
            new_docs.append(SVGDocument(new_doc, start, end, doc.compressed))

    self.docList = new_docs

    return bool(self.docList)

def subset_glyphs(self, s) -> bool:
    if etree is None:
        raise ImportError("No module named 'lxml', required to subset SVG")

    # glyph names (before subsetting)
    glyph_order: List[str] = s.orig_glyph_order
    # map from glyph names to original glyph indices
    rev_orig_glyph_map: Dict[str, int] = s.reverseOrigGlyphMap
    # map from original to new glyph indices (after subsetting)
    glyph_index_map: Dict[int, int] = s.glyph_index_map

    new_docs: List[SVGDocument] = []
    for doc in self.docList:

        glyphs = {
            glyph_order[i] for i in range(doc.startGlyphID, doc.endGlyphID + 1)
        }.intersection(s.glyphs)
        if not glyphs:
            # no intersection: we can drop the whole record
            continue

        svg = etree.fromstring(
            # encode because fromstring dislikes xml encoding decl if input is str.
            # SVG xml encoding must be utf-8 as per OT spec.
            doc.data.encode("utf-8"),
            parser=etree.XMLParser(
                # Disable libxml2 security restrictions to support very deep trees.
                # Without this we would get an error like this:
                # `lxml.etree.XMLSyntaxError: internal error: Huge input lookup`
                # when parsing big fonts e.g. noto-emoji-picosvg.ttf.
                huge_tree=True,
                # ignore blank text as it's not meaningful in OT-SVG; it also prevents
                # dangling tail text after removing an element when pretty_print=True
                remove_blank_text=True,
                # don't replace entities; we don't expect any in OT-SVG and they may
                # aboused for XXE attacks
                resolve_entities=False,
            ),
        )

        elements = group_elements_by_id(svg)
        gids = {rev_orig_glyph_map[g] for g in glyphs}
        element_ids = {f"glyph{i}" for i in gids}
        closure_element_ids(elements, element_ids)

        if not subset_elements(svg, element_ids):
            continue

        if not s.options.retain_gids:
            id_map = remap_glyph_ids(svg, glyph_index_map)
            update_glyph_href_links(svg, id_map)

        new_doc = etree.tostring(svg, pretty_print=s.options.pretty_svg).decode("utf-8")

        new_gids = (glyph_index_map[i] for i in gids)
        for start, end in ranges(new_gids):
            new_docs.append(SVGDocument(new_doc, start, end, doc.compressed))

    self.docList = new_docs

    return bool(self.docList)

--- a/Lib/fontTools/subset/svg.py
+++ b/Lib/fontTools/subset/svg.py
@@ -32,6 +32,9 @@
                 # ignore blank text as it's not meaningful in OT-SVG; it also prevents
                 # dangling tail text after removing an element when pretty_print=True
                 remove_blank_text=True,
+                # don't replace entities; we don't expect any in OT-SVG and they may
+                # aboused for XXE attacks
+                resolve_entities=False,
             ),
         )


Navidrome is an open source web-based music collection server and streamer. Navidrome stores the JWT secret in plaintext in the navidrome.db database file under the property table. This practice introduces a security risk because anyone with access to the database file can retrieve the secret. This vulnerability is fixed in 0.54.1.

Cleartext Storage of Sensitive Information

The product stores sensitive information in cleartext within a resource that might be accessible to another control sphere.

func Init(ds model.DataStore) {
	once.Do(func() {
		log.Info("Setting Session Timeout", "value", conf.Server.SessionTimeout)
		secret, err := ds.Property(context.TODO()).Get(consts.JWTSecretKey)
		if err != nil || secret == "" {
			log.Error("No JWT secret found in DB. Setting a temp one, but please report this error", err)
			secret = uuid.NewString()
		}
		Secret = []byte(secret)
		TokenAuth = jwtauth.New("HS256", Secret, nil)
	})
}

func Init(ds model.DataStore) {
	once.Do(func() {
		ctx := context.TODO()
		log.Info("Setting Session Timeout", "value", conf.Server.SessionTimeout)

		secret, err := ds.Property(ctx).Get(consts.JWTSecretKey)
		if err != nil || secret == "" {
			secret = createNewSecret(ctx, ds)
		} else {
			if secret, err = utils.Decrypt(ctx, getEncKey(), secret); err != nil {
				log.Error(ctx, "Could not decrypt JWT secret, creating a new one", err)
				secret = createNewSecret(ctx, ds)
			}
		}

		TokenAuth = jwtauth.New("HS256", []byte(secret), nil)
	})
}

var (
	once      sync.Once
	TokenAuth *jwtauth.JWTAuth
)

func Decrypt(ctx context.Context, encKey []byte, encData string) (value string, err error) {
	// Recover from any panics
	defer func() {
		if r := recover(); r != nil {
			log.Error(ctx, "Panic during decryption", r)
			err = errors.New("decryption panicked")
		}
	}()

	enc, _ := base64.StdEncoding.DecodeString(encData)

	block, err := aes.NewCipher(encKey)
	if err != nil {
		log.Error(ctx, "Could not create a cipher", err)
		return "", err
	}

	aesGCM, err := cipher.NewGCM(block)
	if err != nil {
		log.Error(ctx, "Could not create a GCM", err)
		return "", err
	}

	nonceSize := aesGCM.NonceSize()
	nonce, ciphertext := enc[:nonceSize], enc[nonceSize:]

	plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		log.Error(ctx, "Could not decrypt password", err)
		return "", err
	}

	return string(plaintext), nil

func createNewSecret(ctx context.Context, ds model.DataStore) string {
	log.Info(ctx, "Creating new JWT secret, used for encrypting UI sessions")
	secret := uuid.NewString()
	encSecret, err := utils.Encrypt(ctx, getEncKey(), secret)
	if err != nil {
		log.Error(ctx, "Could not encrypt JWT secret", err)
	}

	if err := ds.Property(ctx).Put(consts.JWTSecretKey, encSecret); err != nil {
		log.Error(ctx, "Could not save JWT secret in DB", err)
	}

	return secret
}

func getEncKey() []byte {
	key := cmp.Or(
		conf.Server.PasswordEncryptionKey,
		consts.DefaultEncryptionKey,
	)
	sum := sha256.Sum256([]byte(key))
	return sum[:]
}

--- a/core/auth/auth.go
+++ b/core/auth/auth.go
@@ -1,12 +1,18 @@
 func Init(ds model.DataStore) {
 	once.Do(func() {
+		ctx := context.TODO()
 		log.Info("Setting Session Timeout", "value", conf.Server.SessionTimeout)
-		secret, err := ds.Property(context.TODO()).Get(consts.JWTSecretKey)
+
+		secret, err := ds.Property(ctx).Get(consts.JWTSecretKey)
 		if err != nil || secret == "" {
-			log.Error("No JWT secret found in DB. Setting a temp one, but please report this error", err)
-			secret = uuid.NewString()
+			secret = createNewSecret(ctx, ds)
+		} else {
+			if secret, err = utils.Decrypt(ctx, getEncKey(), secret); err != nil {
+				log.Error(ctx, "Could not decrypt JWT secret, creating a new one", err)
+				secret = createNewSecret(ctx, ds)
+			}
 		}
-		Secret = []byte(secret)
-		TokenAuth = jwtauth.New("HS256", Secret, nil)
+
+		TokenAuth = jwtauth.New("HS256", []byte(secret), nil)
 	})
 }

--- /dev/null
+++ b/core/auth/auth.go
@@ -0,0 +1,4 @@
+var (
+	once      sync.Once
+	TokenAuth *jwtauth.JWTAuth
+)

--- /dev/null
+++ b/core/auth/auth.go
@@ -0,0 +1,33 @@
+func Decrypt(ctx context.Context, encKey []byte, encData string) (value string, err error) {
+	// Recover from any panics
+	defer func() {
+		if r := recover(); r != nil {
+			log.Error(ctx, "Panic during decryption", r)
+			err = errors.New("decryption panicked")
+		}
+	}()
+
+	enc, _ := base64.StdEncoding.DecodeString(encData)
+
+	block, err := aes.NewCipher(encKey)
+	if err != nil {
+		log.Error(ctx, "Could not create a cipher", err)
+		return "", err
+	}
+
+	aesGCM, err := cipher.NewGCM(block)
+	if err != nil {
+		log.Error(ctx, "Could not create a GCM", err)
+		return "", err
+	}
+
+	nonceSize := aesGCM.NonceSize()
+	nonce, ciphertext := enc[:nonceSize], enc[nonceSize:]
+
+	plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil)
+	if err != nil {
+		log.Error(ctx, "Could not decrypt password", err)
+		return "", err
+	}
+
+	return string(plaintext), nil

--- /dev/null
+++ b/core/auth/auth.go
@@ -0,0 +1,14 @@
+func createNewSecret(ctx context.Context, ds model.DataStore) string {
+	log.Info(ctx, "Creating new JWT secret, used for encrypting UI sessions")
+	secret := uuid.NewString()
+	encSecret, err := utils.Encrypt(ctx, getEncKey(), secret)
+	if err != nil {
+		log.Error(ctx, "Could not encrypt JWT secret", err)
+	}
+
+	if err := ds.Property(ctx).Put(consts.JWTSecretKey, encSecret); err != nil {
+		log.Error(ctx, "Could not save JWT secret in DB", err)
+	}
+
+	return secret
+}

--- /dev/null
+++ b/core/auth/auth.go
@@ -0,0 +1,8 @@
+func getEncKey() []byte {
+	key := cmp.Or(
+		conf.Server.PasswordEncryptionKey,
+		consts.DefaultEncryptionKey,
+	)
+	sum := sha256.Sum256([]byte(key))
+	return sum[:]
+}

#!/bin/bash
# From ghcr.io/anonymous2578-data/cve-2024-56362:latest
# bash /workspace/fix-run.sh
set -e

cd /workspace/navidrome
git apply --whitespace=nowarn /workspace/test.patch /workspace/fix.patch
go test -timeout 30s -run ^TestAuth$ github.com/navidrome/navidrome/core/auth

#!/bin/bash
# From ghcr.io/anonymous2578-data/cve-2024-56362:latest
# bash /workspace/unit_test.sh
set -e

cd /workspace/navidrome
git apply --whitespace=nowarn /workspace/fix.patch
go test -timeout 30s -run ^TestAuth$ github.com/navidrome/navidrome/core/auth


systeminformation is an npm package that provides system and OS information library for node.js. In systeminformation before version 4.26.2 there is a command injection vulnerability. Problem was fixed in version 4.26.2 with a shell string sanitation fix.

function sanitizeShellString(str) {
  let result = str;
  result = result.replace(/>/g, "");
  result = result.replace(/</g, "");
  result = result.replace(/\*/g, "");
  result = result.replace(/\?/g, "");
  result = result.replace(/\[/g, "");
  result = result.replace(/\]/g, "");
  result = result.replace(/\|/g, "");
  result = result.replace(/\`/g, "");
  result = result.replace(/$/g, "");
  result = result.replace(/;/g, "");
  result = result.replace(/&/g, "");
  result = result.replace(/\)/g, "");
  result = result.replace(/\(/g, "");
  result = result.replace(/\$/g, "");
  result = result.replace(/#/g, "");
  result = result.replace(/\\/g, "");
  return result
}

function sanitizeShellString(str) {
  let result = str;
  result = result.replace(/>/g, "");
  result = result.replace(/</g, "");
  result = result.replace(/\*/g, "");
  result = result.replace(/\?/g, "");
  result = result.replace(/\[/g, "");
  result = result.replace(/\]/g, "");
  result = result.replace(/\|/g, "");
  result = result.replace(/\`/g, "");
  result = result.replace(/$/g, "");
  result = result.replace(/;/g, "");
  result = result.replace(/&/g, "");
  result = result.replace(/\)/g, "");
  result = result.replace(/\(/g, "");
  result = result.replace(/\$/g, "");
  result = result.replace(/#/g, "");
  result = result.replace(/\\/g, "");
  result = result.replace(/\t/g, "");
  result = result.replace(/\n/g, "");
  result = result.replace(/\"/g, "");
  return result
}

--- a/lib/util.js
+++ b/lib/util.js
@@ -16,5 +16,8 @@
   result = result.replace(/\$/g, "");
   result = result.replace(/#/g, "");
   result = result.replace(/\\/g, "");
+  result = result.replace(/\t/g, "");
+  result = result.replace(/\n/g, "");
+  result = result.replace(/\"/g, "");
   return result
 }