Class: SchemaGraphy::AstGate

Inherits:
Object
  • Object
show all
Defined in:
lib/schemagraphy/safe_expression.rb

Overview

Provides a simple, deny-by-exception sandbox for mapping expressions. It validates code by walking the Abstract Syntax Tree (AST) and blocking known dangerous operations, rather than attempting to allowlist safe ones.

Constant Summary collapse

BLOCKED_BAREWORDS =
A list of dangerous bareword methods that are blocked.
%w[
  eval instance_eval class_eval module_eval binding
  require require_relative load autoload
  system exec spawn fork backtick `
  open ObjectSpace GC Thread Process at_exit
].freeze
DISALLOWED_NODES =
A list of AST node types that are explicitly disallowed.
%i[
  # Definitions and meta-programming
  def_node class_node module_node define_node alias_node undef_node
  # Globals and constants paths
  global_variable_read_node constant_path_node
  # Shell and backticks
  x_string_node interpolated_x_string_node
].freeze
DANGEROUS_CONSTANTS =
A list of constants that are considered dangerous and are blocked.
%w[
  Kernel Object Module Class File FileUtils IO Dir Process Open3 PTY Thread
  SystemSignal Signal Gem Net HTTP TCPSocket UDPSocket Socket ObjectSpace GC
].freeze

Class Method Summary collapse

Class Method Details

.validate!(code, context_keys: []) ⇒ Object

Validates the given code by parsing it and walking the AST.

Parameters:

  • code (String)
    The Ruby code to validate.
  • context_keys (Array<Symbol>) (defaults to: [])
    A list of keys available in the execution context.

Raises:

  • (SyntaxError)
    if the code has syntax errors.
  • (SecurityError)
    if the code contains disallowed operations.


41
42
43
44
45
46
# File 'lib/schemagraphy/safe_expression.rb', line 41

def self.validate! code, context_keys: []
  result = Prism.parse(code)
  raise SyntaxError, result.errors.map(&:message).join(', ') if result.errors.any?

  walk(result.value, context_keys: context_keys)
end

.walk(node, context_keys: []) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Recursively walks the AST, checking for disallowed nodes and operations.

Parameters:

  • node (Prism::Node)
    The current AST node.
  • context_keys (Array<Symbol>) (defaults to: [])
    A list of keys available in the execution context.

Raises:

  • (SecurityError)
    if a disallowed operation is found.


54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/schemagraphy/safe_expression.rb', line 54

def self.walk node, context_keys: []
  return unless node.is_a?(Prism::Node)

  type = node.type
  raise SecurityError, "node not allowed: #{type}" if DISALLOWED_NODES.include?(type)

  case node
  when Prism::CallNode
    # Block dangerous barewords (system, eval, etc.)
    if node.receiver.nil? && BLOCKED_BAREWORDS.include?(node.name.to_s)
      raise SecurityError, "method not allowed: #{node.name}"
    end
    # Block dangerous constants and constant paths
    if node.receiver.is_a?(Prism::ConstantReadNode) && DANGEROUS_CONSTANTS.include?(node.receiver.name.to_s)
      raise SecurityError, "unsafe constant: #{node.receiver.name}"
    end
    raise SecurityError, 'unsafe constant path' if node.receiver.is_a?(Prism::ConstantPathNode)

  when Prism::ConstantReadNode
    # Allow only core Ruby constants defined in SafeTransform
    const_name = node.name.to_s
    unless SafeTransform::CORE_CONSTANTS.key?(const_name.to_sym)
      raise SecurityError, "constant not allowed: #{const_name}"
    end
  when Prism::ConstantPathNode, Prism::GlobalVariableReadNode
    raise SecurityError, 'constant paths and global variables are not allowed'
  when Prism::DefNode, Prism::ClassNode, Prism::ModuleNode
    raise SecurityError, 'method, class, and module definitions are not allowed'
  when Prism::BackReferenceReadNode, Prism::XStringNode, Prism::InterpolatedXStringNode
    raise SecurityError, 'shell commands and backticks are not allowed'
  end

  node.child_nodes.each { |child| walk(child, context_keys: context_keys) if child }
end