Skip to content
Closed
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
45 changes: 38 additions & 7 deletions lib/secvault/secrets.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

module Secvault
class Secrets
# Regexp to match unsafe environment variable values (comma, dash, and colon)
UNSAFE_ENV_VALUE_REGEXP = /^\s*[,:-]/

# Define permitted classes for YAML.safe_load - commonly used in Rails secrets
PERMITTED_YAML_CLASSES = [
Symbol,
Expand Down Expand Up @@ -93,11 +96,8 @@

# Read and process the plain YAML file content
source = path.read

# Process ERB and parse YAML - using same method as Rails
erb_result = ERB.new(source).result
secrets = YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(erb_result) : YAML.safe_load(erb_result, aliases: true, permitted_classes: PERMITTED_YAML_CLASSES)


Check failure on line 99 in lib/secvault/secrets.rb

View workflow job for this annotation

GitHub Actions / Code Quality

Layout/TrailingWhitespace: Trailing whitespace detected.
secrets = load_secrets_from_yaml(source)
secrets ||= {}

# Only load environment-specific section (YAML anchors handle sharing)
Expand All @@ -108,15 +108,46 @@
def read_secrets(secrets_path, env)
if secrets_path.exist?
# Handle plain YAML secrets.yml only - using same method as Rails
erb_result = ERB.new(secrets_path.read).result
all_secrets = YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(erb_result) : YAML.safe_load(erb_result, aliases: true, permitted_classes: PERMITTED_YAML_CLASSES)
source = secrets_path.read
all_secrets = load_secrets_from_yaml(source)

env_secrets = all_secrets[env.to_s]
return env_secrets.deep_symbolize_keys if env_secrets
end

{}
end

private

def load_secrets_from_yaml(source)

Check failure on line 123 in lib/secvault/secrets.rb

View workflow job for this annotation

GitHub Actions / Code Quality

Layout/IndentationConsistency: Inconsistent indentation detected.

Check failure on line 123 in lib/secvault/secrets.rb

View workflow job for this annotation

GitHub Actions / Code Quality

Layout/IndentationWidth: Use 2 (not 4) spaces for indentation.
# Process ERB and parse YAML - using same method as Rails but with safe env wrapper

Check failure on line 125 in lib/secvault/secrets.rb

View workflow job for this annotation

GitHub Actions / Code Quality

Layout/TrailingWhitespace: Trailing whitespace detected.
erb_result = nil
with_safe_env do
erb_result = ERB.new(source).result
end

YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(erb_result) : YAML.safe_load(erb_result, aliases: true, permitted_classes: PERMITTED_YAML_CLASSES)
Copy link

Choose a reason for hiding this comment

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

Monkey-patching a global object like ENV is not thread-safe. If multiple threads execute this code concurrently (e.g., in a Puma server), it could lead to race conditions where one thread interferes with another's execution or leaves the ENV object in a corrupted state. A safer, thread-local approach would be to create a custom binding with a proxy object for ENV and pass it to ERB.new(source).result(binding). This would isolate the change without modifying global state.

end

def with_safe_env

Check failure on line 134 in lib/secvault/secrets.rb

View workflow job for this annotation

GitHub Actions / Code Quality

Layout/IndentationConsistency: Inconsistent indentation detected.

Check failure on line 134 in lib/secvault/secrets.rb

View workflow job for this annotation

GitHub Actions / Code Quality

Layout/IndentationWidth: Use 2 (not 4) spaces for indentation.
original_env_method = ENV.method(:[])

ENV.define_singleton_method(:[]) do |key|
Copy link

Choose a reason for hiding this comment

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

The current approach of simply wrapping the value in double quotes might not correctly handle environment variables that already contain special characters like quotes or backslashes, potentially leading to invalid YAML. For a more robust solution, consider using a standard library method for serialization, such as value.to_json or YAML.dump(value).strip, which will handle all necessary escaping correctly.

value = original_env_method.call(key)

return value if value.nil? || value.strip == ''

Check failure on line 140 in lib/secvault/secrets.rb

View workflow job for this annotation

GitHub Actions / Code Quality

Style/StringLiterals: Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

return "\"#{value}\"" if value =~ UNSAFE_ENV_VALUE_REGEXP

Check failure on line 142 in lib/secvault/secrets.rb

View workflow job for this annotation

GitHub Actions / Code Quality

Performance/RegexpMatch: Use `match?` instead of `=~` when `MatchData` is not used.

value
end

Check failure on line 146 in lib/secvault/secrets.rb

View workflow job for this annotation

GitHub Actions / Code Quality

Layout/TrailingWhitespace: Trailing whitespace detected.
yield
ensure
ENV.define_singleton_method(:[], original_env_method)
end
end
end
end
12 changes: 12 additions & 0 deletions spec/secrets_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,18 @@
expect(result[:api_key]).to eq("default_dev_key")
expect(result[:database_url]).to be_nil
end

it "handles unsafe environment variables gracefully" do
with_env_vars({
"DEV_API_KEY" => ",test_value",
"DATABASE_URL" => ":test_url"
}) do
result = described_class.parse([secrets_file], env: "development")

expect(result[:api_key]).to eq(",test_value")
expect(result[:database_url]).to eq(":test_url")
end

Check failure on line 164 in spec/secrets_spec.rb

View workflow job for this annotation

GitHub Actions / Code Quality

Layout/TrailingWhitespace: Trailing whitespace detected.
end
end

context "with multiple files" do
Expand Down