Skip to content

Commit 1f6a709

Browse files
justin808claude
andcommitted
Implement enforce_private_server_bundles security feature and add comprehensive test
Add security enforcement logic: - When enforce_private_server_bundles is enabled, server bundles skip public path fallbacks - Server bundles return private paths even if they don't exist (preventing public fallback) - Add server_bundle_private_path helper that respects server_bundle_output_path configuration - Add enforce_private_server_bundles? helper for clean configuration access Add comprehensive test coverage: - Test that enforcement prevents fallback to public paths when enabled - Mock File.exist? to verify private path is returned even when public path exists - Update mock_bundle_configs to include enforce_private_server_bundles default (false) - All 54 tests pass, including new enforcement test Security benefit: - Prevents accidental serving of server bundles from public directories - Ensures server-side code remains private even when deployment scripts fail - Opt-in feature (defaults to false) for backwards compatibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 5917285 commit 1f6a709

File tree

2 files changed

+43
-0
lines changed

2 files changed

+43
-0
lines changed

lib/react_on_rails/utils.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ def self.bundle_js_file_path(bundle_name)
108108
end
109109

110110
private_class_method def self.handle_missing_manifest_entry(bundle_name)
111+
# For server bundles with enforcement enabled, skip public path fallbacks
112+
return server_bundle_private_path(bundle_name) if server_bundle?(bundle_name) && enforce_private_server_bundles?
113+
111114
# When manifest lookup fails, try multiple fallback locations:
112115
# Build fallback locations conditionally based on packer availability
113116
fallback_locations = []
@@ -141,6 +144,17 @@ def self.bundle_js_file_path(bundle_name)
141144
bundle_name == config.rsc_bundle_js_file
142145
end
143146

147+
private_class_method def self.enforce_private_server_bundles?
148+
ReactOnRails.configuration.enforce_private_server_bundles
149+
end
150+
151+
private_class_method def self.server_bundle_private_path(bundle_name)
152+
config = ReactOnRails.configuration
153+
preferred_dir = config.server_bundle_output_path.presence || "ssr-generated"
154+
root_path = Rails.root || "."
155+
File.expand_path(File.join(root_path, preferred_dir, bundle_name))
156+
end
157+
144158
def self.server_bundle_js_file_path
145159
return @server_bundle_path if @server_bundle_path && !Rails.env.development?
146160

spec/react_on_rails/utils_spec.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ def mock_bundle_configs(server_bundle_name: random_bundle_name, rsc_bundle_name:
7878
.and_return(rsc_bundle_name)
7979
allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_output_path")
8080
.and_return("ssr-generated")
81+
allow(ReactOnRails).to receive_message_chain("configuration.enforce_private_server_bundles")
82+
.and_return(false)
8183
end
8284

8385
def mock_dev_server_running
@@ -168,6 +170,33 @@ def mock_dev_server_running
168170
expect(result).to eq(File.expand_path(env_specific_path))
169171
end
170172
end
173+
174+
context "with enforce_private_server_bundles enabled" do
175+
before do
176+
mock_missing_manifest_entry("server-bundle.js")
177+
allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_js_file")
178+
.and_return("server-bundle.js")
179+
allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_output_path")
180+
.and_return("ssr-generated")
181+
allow(ReactOnRails).to receive_message_chain("configuration.enforce_private_server_bundles")
182+
.and_return(true)
183+
end
184+
185+
it "returns private path and does not fall back to public when enforcement is enabled" do
186+
ssr_generated_path = File.expand_path(File.join(Rails.root.to_s, "ssr-generated", "server-bundle.js"))
187+
public_packs_path = File.expand_path(File.join("public", "packs", "server-bundle.js"))
188+
189+
# Mock File.exist? so SSR-generated path returns false but public path returns true
190+
allow(File).to receive(:exist?).and_call_original
191+
allow(File).to receive(:exist?).with(ssr_generated_path).and_return(false)
192+
allow(File).to receive(:exist?).with(public_packs_path).and_return(true)
193+
194+
result = described_class.bundle_js_file_path("server-bundle.js")
195+
196+
# Should return the private path even though it doesn't exist, not fall back to public
197+
expect(result).to eq(ssr_generated_path)
198+
end
199+
end
171200
end
172201
end
173202

0 commit comments

Comments
 (0)