diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4f1d7e0..5bd3dc0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,6 +1,40 @@ class ApplicationController < ActionController::Base + before_action :strip_null_bytes_from_params + private + def strip_null_bytes_from_params + sanitize_param_object!(request.parameters) if request.parameters.present? + end + + def sanitize_param_object!(value) + case value + when String + value.delete("\u0000") + when Array + value.map! { |item| sanitize_param_object!(item) } + when ActionController::Parameters + # Sanitize both keys and values. + value.keys.each do |key| + item = value.delete(key) + sanitized_key = key.to_s.delete("\u0000") + value[sanitized_key] = sanitize_param_object!(item) + end + value + when Hash + # Sanitize both keys and values, preserving Symbol keys. + value.keys.each do |key| + item = value.delete(key) + sanitized_key_string = key.to_s.delete("\u0000") + sanitized_key = key.is_a?(Symbol) ? sanitized_key_string.to_sym : sanitized_key_string + value[sanitized_key] = sanitize_param_object!(item) + end + value + else + value + end + end + def current_application_settings @current_application_settings ||= ApplicationSetting.get_current_app_settings end diff --git a/spec/requests/user_registrations_spec.rb b/spec/requests/user_registrations_spec.rb new file mode 100644 index 0000000..e2743b1 --- /dev/null +++ b/spec/requests/user_registrations_spec.rb @@ -0,0 +1,48 @@ +require 'rails_helper' + +RSpec.describe 'User registrations', type: :request do + describe 'POST /users' do + let(:base_email) { "new_user_#{SecureRandom.hex(4)}@example.com" } + let(:password) { 'password123' } + + it 'sanitizes null bytes in registration params and does not raise an error' do + expect do + post user_registration_path, params: { + user: { + email: "#{base_email}\u0000", + password: "pass\u0000word123", + password_confirmation: "pass\u0000word123" + } + } + end.not_to raise_error + + expect(response).to have_http_status(:see_other) + expect(response).to redirect_to(root_path) + expect(User.exists?(email: base_email)).to be(true) + end + + it 'sanitizes null bytes recursively for nested registration payloads' do + nested_params = { + user: { + email: "#{base_email}\u0000", + password: "pass\u0000word123", + password_confirmation: "pass\u0000word123", + metadata: { + tags: ["fir\u0000st", "sec\u0000ond"], + profile: { + nickname: "ni\u0000ck" + } + } + } + } + + expect do + post user_registration_path, params: nested_params + end.not_to raise_error + + expect(response).to have_http_status(:see_other) + expect(response).to redirect_to(root_path) + expect(User.exists?(email: base_email)).to be(true) + end + end +end diff --git a/spec/requests/user_sessions_spec.rb b/spec/requests/user_sessions_spec.rb new file mode 100644 index 0000000..cb2f18c --- /dev/null +++ b/spec/requests/user_sessions_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +RSpec.describe 'User sessions', type: :request do + describe 'POST /users/sign_in' do + let(:user) { create(:user) } + let(:email_with_null_byte) { "#{user.email}\u0000" } + it 'sanitizes null bytes in login params and does not raise an error' do + expect do + post user_session_path, params: { + user: { + email: email_with_null_byte, + password: user.password + } + } + end.not_to raise_error + + expect(response).to have_http_status(:see_other) + expect(response).to redirect_to(root_path) + end + + it 'sanitizes null bytes recursively in nested params and arrays' do + nested_params = { + user: { + email: "#{user.email}\u0000", + password: "pass\u0000word123", + metadata: { + tags: ["alp\u0000ha", "be\u0000ta"], + profile: { + nickname: "ni\u0000ck" + } + } + } + } + + expect do + post user_session_path, params: nested_params + end.not_to raise_error + + expect(response).to have_http_status(:see_other) + expect(response).to redirect_to(root_path) + end + + it 'sanitizes null bytes in password before authentication' do + expect do + post user_session_path, params: { + user: { + email: user.email, + password: "pass\u0000word123" + } + } + end.not_to raise_error + + expect(response).to have_http_status(:see_other) + expect(response).to redirect_to(root_path) + end + end +end diff --git a/yarn.lock b/yarn.lock index 4c10751..324735b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -239,9 +239,9 @@ bootstrap@^5.3.3: integrity sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg== brace-expansion@^1.1.7: - version "1.1.12" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" - integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== + version "1.1.13" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.13.tgz#d37875c01dc9eff988dd49d112a57cb67b54efe6" + integrity sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" @@ -594,9 +594,9 @@ picocolors@^1.0.0: integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + version "2.3.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601" + integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA== pify@^2.3.0: version "2.3.0" @@ -815,9 +815,9 @@ yallist@^4.0.0: integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yaml@^2.3.4: - version "2.3.4" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" - integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== + version "2.8.3" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.3.tgz#a0d6bd2efb3dd03c59370223701834e60409bd7d" + integrity sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg== yargs-parser@^21.1.1: version "21.1.1"