Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions openc3/lib/openc3/script/api_shared.rb
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@
start_time = Time.now.sys
success, value = _openc3_script_wait_implementation_comparison(target_name, packet_name, item_name, type, comparison_to_eval, timeout, polling_rate, scope: scope, token: token, &block)
value = "'#{value}'" if value.is_a? String # Show user the check against a quoted string
value = 'nil' if value.nil? # Show user nil value as 'nil'
time_diff = Time.now.sys - start_time
check_str = "CHECK: #{_upcase(target_name, packet_name, item_name)}"
if comparison_to_eval
Expand Down Expand Up @@ -531,7 +532,7 @@
if comparison_to_eval
_check_eval(target_name, packet_name, item_name, comparison_to_eval, value)
else
puts "CHECK: #{_upcase(target_name, packet_name, item_name)} == #{value}"
puts "CHECK: #{_upcase(target_name, packet_name, item_name)} == #{value.nil? ? 'nil' : value.inspect}"
end
end

Expand Down Expand Up @@ -632,6 +633,7 @@
start_time = Time.now.sys
success, value = _openc3_script_wait_implementation_comparison(target_name, packet_name, item_name, value_type, comparison_to_eval, timeout, polling_rate, scope: scope, token: token)
value = "'#{value}'" if value.is_a? String # Show user the check against a quoted string
value = 'nil' if value.nil? # Show user nil value as 'nil'
time_diff = Time.now.sys - start_time
wait_str = "WAIT: #{_upcase(target_name, packet_name, item_name)} #{comparison_to_eval}"
value_str = "with value == #{value} after waiting #{time_diff} seconds"
Expand Down Expand Up @@ -863,8 +865,20 @@
# Show user the check against a quoted string
# Note: We have to preserve the original 'value' variable because we're going to eval against it
value_str = value.is_a?(String) ? "'#{value}'" : value
value_str = 'nil' if value.nil? # Show user nil value as 'nil'
with_value = "with value == #{value_str}"
if eval(string)

eval_is_valid = _check_eval_validity(value, comparison_to_eval)
unless eval_is_valid
message = "Invalid comparison for types"
if $disconnect
puts "ERROR: #{message}"
else
raise CheckError, message
end
end

if eval_is_valid && eval(string)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remote Code Execution possible via eval()-type functions - critical severity
Using functions such as eval can lead to users being able to run their own code on your servers.

Show fix

Remediation: If possible, avoid using these functions altogether. If not, use a list of allowed inputs that can feed into these functions.

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

puts "#{check_str} success #{with_value}"
else
message = "#{check_str} failed #{with_value}"
Expand All @@ -883,5 +897,28 @@
raise e
end
end

def _check_eval_validity(value, comparison)
return true if comparison.nil? || comparison.empty?

begin
operator, operand = extract_operator_and_operand_from_comparison(comparison)
rescue RuntimeError => e
if e.message.include?("Unable to parse operand")
# If we can't parse the operand, let the eval happen anyway
# It will raise an appropriate error (like NameError for undefined constants)
return true
end
raise # Re-raise invalid operator errors
rescue JSON::ParserError
return true
end

if [">=", "<=", ">", "<"].include?(operator)

Check warning on line 917 in openc3/lib/openc3/script/api_shared.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Merge this "if" statement with the nested one.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAgFGVh17VRJvTZR&open=AZ1GZAgFGVh17VRJvTZR&pullRequest=3143
return false if value.nil? || operand.nil? || value.is_a?(Array) || operand.is_a?(Array)
end

return true
end
end
end
59 changes: 46 additions & 13 deletions openc3/lib/openc3/script/extract.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# This file may also be used under the terms of a commercial license
# if purchased from OpenC3, Inc.

require 'json'
require 'openc3/utilities/store'

module OpenC3
Expand Down Expand Up @@ -154,22 +155,54 @@
end

def extract_fields_from_check_text(text)
split_string = text.split
raise "ERROR: Check improperly specified: #{text}" if split_string.length < 3
target_name, packet_name, item_name, comparison = text.split(nil, 4) # Ruby: second split arg is max number of resultant elements
raise "ERROR: Check improperly specified: #{text}" if item_name.nil?

Check warning on line 159 in openc3/lib/openc3/script/extract.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a specific exception class instead of raising a string literal.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAjlGVh17VRJvTZS&open=AZ1GZAjlGVh17VRJvTZS&pullRequest=3143

target_name = split_string[0]
packet_name = split_string[1]
item_name = split_string[2]
comparison_to_eval = nil
return [target_name, packet_name, item_name, comparison_to_eval] if split_string.length == 3
raise "ERROR: Check improperly specified: #{text}" if split_string.length < 4
# comparison is either nil, the comparison string, or an empty string.
# We need it to not be an empty string.
comparison = nil if comparison&.length == 0

operator, _ = comparison&.split(nil, 2)

Check warning on line 165 in openc3/lib/openc3/script/extract.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the trailing underscore from this multiple assignment.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAjlGVh17VRJvTZT&open=AZ1GZAjlGVh17VRJvTZT&pullRequest=3143
raise "ERROR: Use '==' instead of '=' in #{text}" if operator == "="

Check warning on line 166 in openc3/lib/openc3/script/extract.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a specific exception class instead of raising a string literal.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAjlGVh17VRJvTZU&open=AZ1GZAjlGVh17VRJvTZU&pullRequest=3143

return [target_name, packet_name, item_name, comparison]
end

# Splits `check()` comparison expressions, e.g. "== 'foo bar'" becomes ["==", "foo bar"]
def extract_operator_and_operand_from_comparison(comparison)
valid_operators = ["==", "!=", ">=", "<=", ">", "<", "in"]

split_string = text.split(/ /) # Split on regex spaces to preserve spaces in comparison
index = split_string.rindex(item_name)
comparison_to_eval = split_string[(index + 1)..(split_string.length - 1)].join(" ")
raise "ERROR: Use '==' instead of '=': #{text}" if split_string[3] == '='
operator, operand = comparison.split(nil, 2) # Ruby: second split arg is max number of resultant elements

return [target_name, packet_name, item_name, comparison_to_eval]
if operand.nil?
# Don't allow operator without operand
raise "ERROR: Invalid comparison, must specify an operand: #{comparison}" if !operator.nil?

Check warning on line 179 in openc3/lib/openc3/script/extract.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a specific exception class instead of raising a string literal.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAjlGVh17VRJvTZV&open=AZ1GZAjlGVh17VRJvTZV&pullRequest=3143
return [nil, nil]
end

raise "ERROR: Invalid operator: '#{operator}'" unless valid_operators.include?(operator)

Check warning on line 183 in openc3/lib/openc3/script/extract.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a specific exception class instead of raising a string literal.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAjlGVh17VRJvTZW&open=AZ1GZAjlGVh17VRJvTZW&pullRequest=3143

# Handle string operand: remove surrounding double/single quotes
if operand.match?(/^(['"])(.*)\1$/m) # Starts with single or double quote, and ends with matching quote
operand = operand[1..-2]
return [operator, operand]
end

# Handle other operand types
if operand == "nil"
operand = nil
elsif operand == "false"
operand = false
elsif operand == "true"
operand = true
else
begin
operand = JSON.parse(operand)
rescue JSON::ParserError
raise "ERROR: Unable to parse operand: #{operand}"

Check warning on line 202 in openc3/lib/openc3/script/extract.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a specific exception class instead of raising a string literal.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AZ1GZAjlGVh17VRJvTZX&open=AZ1GZAjlGVh17VRJvTZX&pullRequest=3143
end
end
return [operator, operand]
end
end
end
43 changes: 38 additions & 5 deletions openc3/python/openc3/api/api_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# This file may also be used under the terms of a commercial license
# if purchased from OpenC3, Inc.

import json
import sys
import time
import traceback
Expand All @@ -19,6 +20,7 @@
from openc3.utilities.extract import (
extract_fields_from_check_text,
extract_fields_from_tlm_text,
extract_operator_and_operand_from_comparison,
)


Expand Down Expand Up @@ -965,16 +967,21 @@ def _check_eval(target_name, packet_name, item_name, comparison_to_eval, value):
else:
value_str = value
with_value = f"with value == {value_str}"

try:
if eval(string):
eval_is_valid = _check_eval_validity(value, comparison_to_eval)
if eval_is_valid and eval(string):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsafe eval usage can lead to remote code execution - critical severity
Using eval on expressions based on user input can execute arbitrary code.

Show fix

Remediation: Consider using ast.literal_eval as an alternative. If that is not possible, replace the usage with a safer alternative that strictly parses the expected input format.

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

print(f"{check_str} success {with_value}")
else:
message = f"{check_str} failed {with_value}"
raise CheckError(message)
except NameError as error:
parts = error.args[0].split("'")
new_error = NameError(f"Uninitialized constant {parts[1]}. Did you mean '{parts[1]}' as a string?")
raise new_error from error
except (NameError, json.JSONDecodeError) as error:
if isinstance(error, NameError):
parts = error.args[0].split("'")
new_error = NameError(f"Uninitialized constant {parts[1]}. Did you mean '{parts[1]}' as a string?")
raise new_error from error
else:
raise


def _frange(value):
Expand All @@ -986,6 +993,32 @@ def _frange(value):
return value


def _check_eval_validity(value, comparison):
if not comparison:
return True

try:
operator, operand = extract_operator_and_operand_from_comparison(comparison)
except RuntimeError as e:
if "Unable to parse operand" in str(e):
# If we can't parse the operand, let the eval happen anyway
# It will raise an appropriate error (like NameError for undefined constants)
return True
raise # Re-raise invalid operator errors
except json.JSONDecodeError:
return True

if operator in [">=", "<=", ">", "<"] and (
value is None or operand is None or isinstance(value, list) or isinstance(operand, list)
):
return False

# Ruby doesn't have the "in" operator
return not (
operator == "in" and (isinstance(operand, str) and not isinstance(value, str) or not isinstance(operand, list))
)


# Interesting formatter to a specific number of significant digits:
# https://stackoverflow.com/questions/3410976/how-to-round-a-number-to-significant-figures-in-python?rq=3
# def format(value, sigfigs=9):
Expand Down
38 changes: 37 additions & 1 deletion openc3/python/openc3/script/api_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# This file may also be used under the terms of a commercial license
# if purchased from OpenC3, Inc.

import json
import sys
import time
import traceback
Expand All @@ -19,6 +20,7 @@
from openc3.utilities.extract import (
extract_fields_from_check_text,
extract_fields_from_tlm_text,
extract_operator_and_operand_from_comparison,
)

from .exceptions import CheckError
Expand Down Expand Up @@ -1046,8 +1048,16 @@ def _check_eval(target_name, packet_name, item_name, comparison_to_eval, value):
else:
value_str = value
with_value = f"with value == {value_str}"

eval_is_valid = _check_eval_validity(value, comparison_to_eval)
if not eval_is_valid:
message = "Invalid comparison for types"
if openc3.script.DISCONNECT:
print(f"ERROR: {message}")
else:
raise CheckError(message)
try:
if eval(string):
if eval_is_valid and eval(string):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsafe eval usage can lead to remote code execution - critical severity
Using eval on expressions based on user input can execute arbitrary code.

Show fix

Remediation: Consider using ast.literal_eval as an alternative. If that is not possible, replace the usage with a safer alternative that strictly parses the expected input format.

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

print(f"{check_str} success {with_value}")
else:
message = f"{check_str} failed {with_value}"
Expand All @@ -1070,6 +1080,32 @@ def _frange(value):
return value


def _check_eval_validity(value, comparison):
if not comparison:
return True

try:
operator, operand = extract_operator_and_operand_from_comparison(comparison)
except RuntimeError as e:
if "Unable to parse operand" in str(e):
# If we can't parse the operand, let the eval happen anyway
# It will raise an appropriate error (like NameError for undefined constants)
return True
raise # Re-raise invalid operator errors
except json.JSONDecodeError:
return True

if operator in [">=", "<=", ">", "<"] and (
value is None or operand is None or isinstance(value, list) or isinstance(operand, list)
):
return False

# Ruby doesn't have the "in" operator
return not (
operator == "in" and (isinstance(operand, str) and not isinstance(value, str) or not isinstance(operand, list))
)


# Interesting formatter to a specific number of significant digits:
# https://stackoverflow.com/questions/3410976/how-to-round-a-number-to-significant-figures-in-python?rq=3
# def format(value, sigfigs=9):
Expand Down
Loading
Loading