Skip to content

exec: a sudo denial with no error code produces no hint #353

Description

Summary

A sudo denial that carries no error code reaches the terminal with nothing actionable beside it. The plugin prints Alpacon denied this sudo command.—no code, no parenthesis—and neither the hint table nor the unknown-code fallback matches that line, so the user gets the bare denial and no next step.

Where

sudoDenialHint (cmd/exec/run.go:235-247) has two paths, and both anchor on sudoDenialLinePrefix:

const sudoDenialLinePrefix = "Alpacon denied this sudo command ("
  • the table scan, via denialCodePresent(output, h.code)sudoDenialLinePrefix + code + ")."
  • the fallback, via firstDenialCode(output)strings.Cut(rest, sudoDenialLinePrefix)

The prefix ends in (, so a line without the code slot matches neither and sudoDenialHint returns "".

When the code is absent

Two branches on the agent side, both reached on a real denial:

  • alpacon_approval.c:449-451g_plugin_printf(SUDO_CONV_ERROR_MSG, "Alpacon denied this sudo command.\n") when error_code_buf is empty
  • pam_alpamon.c:643-645pam_error(pamh, "Alpacon denied this sudo command.") when code[0] == '\0'

error_code_buf ends up empty in two cases: the server sent no error_code, or the sanitizer rejected the one it sent. The sanitizer (alpacon_approval.c:380-404) rejects the code whole rather than truncating it—on any character outside [A-Z0-9_], and on overflow past sizeof(error_code_buf) - 1 on a char[64], with the comment "a truncated code could be spoofed by prefixing a legitimate code with padding". So a server that ships a malformed or over-63-character code lands on this silent path, not on the unknown-code fallback that #344 added for exactly that drift.

Not a regression

Pre-existing: the old table scan missed this line too. TestSudoDenialHint/prefix without the code slot (cmd/exec/sudo_hint_test.go:293) pins the current behavior as deliberate—the fallback must not fire on a line with no code to name.

Raised on #344 as a non-blocking suggestion and left out of scope. It is the sibling of the case that PR's fallback exists to close, and after it, the only remaining path where a real denial reaches the terminal with nothing actionable.

Suggested direction

A generic branch on the codeless literal, after the table scan and the firstDenialCode fallback both miss:

if strings.Contains(output, "Alpacon denied this sudo command.") {
	return denialHintLine("sudo was denied, and the server named no reason. Read the denial in the Alpacon console (web).\n")
}

With no category from the server there is genuinely less to say, so the guidance is thin by necessity—the point is that the user is pointed somewhere instead of nowhere.

Worth settling before writing it: this matches on a bare literal rather than on the plugin's exact line shape, so a command's own output containing that sentence would produce the hint. The existing detectors all anchor on the code slot precisely to keep command output from forging a denial signal; the codeless line has no slot to anchor on. sudoDenialHint is only reachable from a RemoteCommandError, which bounds the exposure but does not remove it.

Acceptance

  • A denial line with no code slot produces a hint pointing at the Alpacon console
  • The existing table scan and unknown-code fallback still win when a code is present
  • TestSudoDenialHint/prefix without the code slot updated to the new expectation, and the code slot without the prefix still yields no hint
  • Forged-input case covered: decide and pin what a command printing the codeless sentence in its own output should do

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions