Skip to content

Commit 35efbda

Browse files
authored
Merge pull request #12 from github/copilot/fix-11
Audit and improve security related tests: eliminate code duplication and implement missing tests
2 parents 3e60c5b + 129982c commit 35efbda

File tree

7 files changed

+85
-45
lines changed

7 files changed

+85
-45
lines changed

lib/hooks/app/auth/auth.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ def validate_auth!(payload, headers, endpoint_config)
2020
auth_config = endpoint_config[:auth]
2121

2222
# Security: Ensure auth type is present and valid
23-
unless auth_config&.dig(:type)&.is_a?(String)
23+
auth_type = auth_config&.dig(:type)
24+
unless auth_type&.is_a?(String) && !auth_type.strip.empty?
2425
error!("authentication configuration missing or invalid", 500)
2526
end
2627

27-
auth_plugin_type = auth_config[:type].downcase
28+
auth_plugin_type = auth_type.downcase
2829

2930
auth_class = nil
3031

lib/hooks/app/helpers.rb

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "securerandom"
4+
require_relative "../security"
45

56
module Hooks
67
module App
@@ -121,13 +122,7 @@ def valid_handler_class_name?(class_name)
121122
return false unless class_name.match?(/\A[A-Z][a-zA-Z0-9_]*\z/)
122123

123124
# Must not be a system/built-in class name
124-
dangerous_classes = %w[
125-
File Dir Kernel Object Class Module Proc Method
126-
IO Socket TCPSocket UDPSocket BasicSocket
127-
Process Thread Fiber Mutex ConditionVariable
128-
Marshal YAML JSON Pathname
129-
]
130-
return false if dangerous_classes.include?(class_name)
125+
return false if Hooks::Security::DANGEROUS_CLASSES.include?(class_name)
131126

132127
true
133128
end

lib/hooks/core/config_validator.rb

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "dry-schema"
4+
require_relative "../security"
45

56
module Hooks
67
module Core
@@ -119,13 +120,7 @@ def self.valid_handler_name?(handler_name)
119120
return false unless handler_name.match?(/\A[A-Z][a-zA-Z0-9_]*\z/)
120121

121122
# Must not be a system/built-in class name
122-
dangerous_classes = %w[
123-
File Dir Kernel Object Class Module Proc Method
124-
IO Socket TCPSocket UDPSocket BasicSocket
125-
Process Thread Fiber Mutex ConditionVariable
126-
Marshal YAML JSON Pathname
127-
]
128-
return false if dangerous_classes.include?(handler_name)
123+
return false if Hooks::Security::DANGEROUS_CLASSES.include?(handler_name)
129124

130125
true
131126
end

lib/hooks/security.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# frozen_string_literal: true
2+
3+
module Hooks
4+
module Security
5+
# List of dangerous class names that should not be loaded as handlers
6+
# for security reasons. These classes provide system access that could
7+
# be exploited if loaded dynamically.
8+
#
9+
# @return [Array<String>] Array of dangerous class names
10+
DANGEROUS_CLASSES = %w[
11+
File Dir Kernel Object Class Module Proc Method
12+
IO Socket TCPSocket UDPSocket BasicSocket
13+
Process Thread Fiber Mutex ConditionVariable
14+
Marshal YAML JSON Pathname
15+
].freeze
16+
end
17+
end

spec/unit/lib/hooks/app/auth/auth_security_spec.rb

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require_relative "../../../../spec_helper"
44

55
describe Hooks::App::Auth do
6+
let(:log) { instance_double(Logger).as_null_object }
67
let(:test_class) do
78
Class.new do
89
include Hooks::App::Auth
@@ -17,6 +18,10 @@ def error!(message, code)
1718
let(:payload) { '{"test": "data"}' }
1819
let(:headers) { { "Content-Type" => "application/json" } }
1920

21+
before(:each) do
22+
Hooks::Log.instance = log
23+
end
24+
2025
describe "#validate_auth!" do
2126
context "when testing security vulnerabilities" do
2227
context "with missing auth configuration" do
@@ -63,39 +68,77 @@ def error!(message, code)
6368
end
6469

6570
it "rejects request with empty string type" do
66-
# TODO
71+
endpoint_config = { auth: { type: "" } }
72+
73+
expect do
74+
instance.validate_auth!(payload, headers, endpoint_config)
75+
end.to raise_error(StandardError, /authentication configuration missing or invalid/)
6776
end
6877
end
6978

7079
context "with missing secret configuration" do
7180
it "rejects request with missing secret_env_key" do
72-
# TODO
81+
endpoint_config = { auth: { type: "hmac" } }
82+
83+
expect do
84+
instance.validate_auth!(payload, headers, endpoint_config)
85+
end.to raise_error(StandardError, /authentication failed/)
7386
end
7487

7588
it "rejects request with nil secret_env_key" do
76-
# TODO
89+
endpoint_config = { auth: { type: "hmac", secret_env_key: nil } }
90+
91+
expect do
92+
instance.validate_auth!(payload, headers, endpoint_config)
93+
end.to raise_error(StandardError, /authentication failed/)
7794
end
7895

7996
it "rejects request with empty secret_env_key" do
80-
# TODO
97+
endpoint_config = { auth: { type: "hmac", secret_env_key: "" } }
98+
99+
expect do
100+
instance.validate_auth!(payload, headers, endpoint_config)
101+
end.to raise_error(StandardError, /authentication failed/)
81102
end
82103

83104
it "rejects request with whitespace-only secret_env_key" do
84-
# TODO
105+
endpoint_config = { auth: { type: "hmac", secret_env_key: " " } }
106+
107+
expect do
108+
instance.validate_auth!(payload, headers, endpoint_config)
109+
end.to raise_error(StandardError, /authentication failed/)
85110
end
86111

87112
it "rejects request with non-string secret_env_key" do
88-
# TODO
113+
endpoint_config = { auth: { type: "hmac", secret_env_key: 123 } }
114+
115+
expect do
116+
instance.validate_auth!(payload, headers, endpoint_config)
117+
end.to raise_error(StandardError, /authentication failed/)
89118
end
90119
end
91120

92121
context "with missing environment variable" do
93122
it "uses generic error message for missing secrets" do
94-
# TODO
123+
ENV.delete("NONEXISTENT_SECRET")
124+
endpoint_config = { auth: { type: "hmac", secret_env_key: "NONEXISTENT_SECRET" } }
125+
126+
expect do
127+
instance.validate_auth!(payload, headers, endpoint_config)
128+
end.to raise_error(StandardError, /authentication failed/)
95129
end
96130

97131
it "does not leak the environment variable name in error" do
98-
# TODO
132+
ENV.delete("SECRET_WEBHOOK_KEY")
133+
endpoint_config = { auth: { type: "hmac", secret_env_key: "SECRET_WEBHOOK_KEY" } }
134+
135+
expect do
136+
instance.validate_auth!(payload, headers, endpoint_config)
137+
end.to raise_error do |error|
138+
# Ensure error message is generic and doesn't leak the environment variable name
139+
expect(error.message).not_to include("SECRET_WEBHOOK_KEY")
140+
expect(error.message).to match(/authentication failed/)
141+
end
99142
end
100143
end
101144

spec/unit/lib/hooks/app/helpers_security_spec.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@ def env
3737
describe "#load_handler security" do
3838
context "with malicious handler class names" do
3939
it "rejects system class names" do
40-
dangerous_classes = %w[File Dir Kernel Object Class Module Proc Method]
41-
42-
dangerous_classes.each do |class_name|
40+
Hooks::Security::DANGEROUS_CLASSES.each do |class_name|
4341
expect do
4442
instance.load_handler(class_name, handler_dir)
4543
end.to raise_error(StandardError, /invalid handler class name/)
@@ -48,6 +46,8 @@ def env
4846

4947
it "rejects network-related class names" do
5048
network_classes = %w[IO Socket TCPSocket UDPSocket BasicSocket]
49+
# Verify these are all in our dangerous classes list
50+
network_classes.each { |cls| expect(Hooks::Security::DANGEROUS_CLASSES).to include(cls) }
5151

5252
network_classes.each do |class_name|
5353
expect do
@@ -58,6 +58,8 @@ def env
5858

5959
it "rejects process and system class names" do
6060
system_classes = %w[Process Thread Fiber Mutex ConditionVariable]
61+
# Verify these are all in our dangerous classes list
62+
system_classes.each { |cls| expect(Hooks::Security::DANGEROUS_CLASSES).to include(cls) }
6163

6264
system_classes.each do |class_name|
6365
expect do
@@ -68,6 +70,8 @@ def env
6870

6971
it "rejects serialization class names" do
7072
serialization_classes = %w[Marshal YAML JSON Pathname]
73+
# Verify these are all in our dangerous classes list
74+
serialization_classes.each { |cls| expect(Hooks::Security::DANGEROUS_CLASSES).to include(cls) }
7175

7276
serialization_classes.each do |class_name|
7377
expect do

spec/unit/lib/hooks/core/config_validator_security_spec.rb

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,9 @@
2525
end
2626

2727
it "rejects dangerous system class names" do
28-
dangerous_configs = [
29-
{ path: "/webhook", handler: "File" },
30-
{ path: "/webhook", handler: "Dir" },
31-
{ path: "/webhook", handler: "Kernel" },
32-
{ path: "/webhook", handler: "Object" },
33-
{ path: "/webhook", handler: "Class" },
34-
{ path: "/webhook", handler: "Module" },
35-
{ path: "/webhook", handler: "Proc" },
36-
{ path: "/webhook", handler: "Method" },
37-
{ path: "/webhook", handler: "IO" },
38-
{ path: "/webhook", handler: "Socket" },
39-
{ path: "/webhook", handler: "TCPSocket" },
40-
{ path: "/webhook", handler: "Process" },
41-
{ path: "/webhook", handler: "Thread" },
42-
{ path: "/webhook", handler: "Marshal" },
43-
{ path: "/webhook", handler: "YAML" },
44-
{ path: "/webhook", handler: "JSON" }
45-
]
28+
dangerous_configs = Hooks::Security::DANGEROUS_CLASSES.map do |class_name|
29+
{ path: "/webhook", handler: class_name }
30+
end
4631

4732
dangerous_configs.each do |config|
4833
expect do

0 commit comments

Comments
 (0)