Class: Rubino::Security::CommandAllowlist

Inherits:
Object
  • Object
show all
Defined in:
lib/rubino/security/command_allowlist.rb

Overview

Manages a whitelist of shell commands that can be executed without confirmation.

An allowlist entry pre-approves an EXACT single command, never a prefix of a larger compound line. A naive ‘start_with?` (the old behaviour) let any line whose head matched an entry auto-resolve to :allow — INCLUDING the chained tail: with `git status` allowlisted, `git status; echo k >> ~/.ssh/authorized_keys` resolved to :allow, turning a read-only pre-approval into headless RCE/exfil. So this matcher is chain-aware, mirroring ReadonlyCommands:

- DangerousPatterns runs FIRST on the whole line, so a dangerous tail
  (curl|sh, recursive rm, write into ~/.ssh, ...) can never be beaten
  by an allowlisted head;
- the line is split into chain segments (|, ||, &&, ;, newline) with the
  same quote-aware splitter as ReadonlyCommands, which REJECTS the line
  outright on redirection (>), backgrounding (&), command substitution
  ($(...) / backticks) or process substitution (<(...) / >()) — the
  constructs that smuggle a write or an execution past a head check;
- EVERY segment must match an allowlist entry, and a match is on a TOKEN
  boundary (a prefix of token tokens), never a bare substring: `git`
  allowlisted does NOT pre-approve `git-secret-leak`, and `git status`
  does NOT pre-approve `git statusxyz`;
- a matched head is FLAG-VETTED via ReadonlyCommands: an allowlisted
  read-capable head can not smuggle a write/exec flag past the prefix
  match. With `git diff` allowlisted, `git diff --output /tmp/PWN`
  (an arbitrary write) is REJECTED — same for `git diff -O...`,
  `find -exec/-delete/-fprintf`, `date -s`, `tree -o` (SEC-1).

Instance Method Summary collapse

Constructor Details

#initialize(config: nil) ⇒ CommandAllowlist

Returns a new instance of CommandAllowlist.



36
37
38
39
# File 'lib/rubino/security/command_allowlist.rb', line 36

def initialize(config: nil)
  @config = config || Rubino.configuration
  @allowlist = @config.security_command_allowlist
end

Instance Method Details

#allowed?(command) ⇒ Boolean

Returns true ONLY when the ENTIRE command line is covered by the allowlist: not dangerous, splits cleanly into chain segments, and every segment’s head matches an allowlist entry on a token boundary.

An EMPTY allowlist matches NOTHING — pre-approval is opt-in, so an unconfigured allowlist must never auto-approve everything.

Returns:

  • (Boolean)


47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/rubino/security/command_allowlist.rb', line 47

def allowed?(command)
  return false if @allowlist.empty?
  return false if DangerousPatterns.dangerous?(command)

  entries = allowlist_token_lists
  return false if entries.empty?

  segments = ReadonlyCommands.split_segments(command.to_s)
  return false if segments.nil? || segments.empty?

  segments.all? { |segment| segment_allowed?(segment, entries) }
end