From dd297ba59f323aba7acca2896bb2045f9f60b28b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Thu, 5 Mar 2026 18:21:17 -0600 Subject: [PATCH 1/2] Fix CI because Brakeman mismatch --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 98d163e..c040098 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -101,7 +101,7 @@ GEM bootstrap-sass (3.4.1) autoprefixer-rails (>= 5.2.1) sassc (>= 2.0.0) - brakeman (8.0.2) + brakeman (8.0.4) racc builder (3.3.0) capybara (3.40.0) From b535ade7d0701d57830740dc168eaded30393194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Thu, 5 Mar 2026 18:14:17 -0600 Subject: [PATCH 2/2] Fix Slack auth filter not halting request on invalid signature `valid_slack_request?` returned a boolean but never called `head` or `render`, so unauthenticated Slack requests were silently allowed through. Added `head :unauthorized and return` to halt the filter chain, consistent with the fix applied in #66. --- .../slack/application_controller.rb | 2 +- .../slack/puzzles_controller_test.rb | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 test/controllers/slack/puzzles_controller_test.rb diff --git a/app/controllers/slack/application_controller.rb b/app/controllers/slack/application_controller.rb index e587022..e70ebb4 100644 --- a/app/controllers/slack/application_controller.rb +++ b/app/controllers/slack/application_controller.rb @@ -7,7 +7,7 @@ class Slack::ApplicationController < ApplicationController private def valid_slack_request? - @verified ||= verify_slack_signature + head :unauthorized and return unless verify_slack_signature end def verify_slack_signature diff --git a/test/controllers/slack/puzzles_controller_test.rb b/test/controllers/slack/puzzles_controller_test.rb new file mode 100644 index 0000000..bd92804 --- /dev/null +++ b/test/controllers/slack/puzzles_controller_test.rb @@ -0,0 +1,56 @@ +require "test_helper" + +class Slack::PuzzlesControllerTest < ActionDispatch::IntegrationTest + setup do + ENV["SLACK_SIGNING_SECRET"] = "test_signing_secret" + end + + test "rejects request with invalid Slack signature" do + post slack_puzzle_path, params: { payload: puzzle_payload }, + headers: slack_headers(secret: "wrong_secret") + + assert_response :unauthorized + end + + test "rejects request with expired Slack timestamp" do + post slack_puzzle_path, params: { payload: puzzle_payload }, + headers: slack_headers(timestamp: Time.now.to_i - 400) + + assert_response :unauthorized + end + + test "allows request with valid Slack signature" do + # Use a payload that fails model validation so no Slack API call is made, + # but the controller still renders 200 (its behavior on both save success/failure). + params = { payload: puzzle_payload(question: "") } + + post slack_puzzle_path, params: params, + headers: slack_headers(body: params.to_query) + + assert_response :ok + end + + private + + def puzzle_payload(question: "What is Ruby?") + { + user: { id: "U123" }, + view: { + state: { + values: { + question: { question: { value: question } }, + answer: { answer: { selected_option: { value: "ruby" } } }, + explanation: { explanation: { value: "It is a programming language." } }, + link: { link: { value: nil } } + } + } + } + }.to_json + end + + def slack_headers(secret: ENV["SLACK_SIGNING_SECRET"], timestamp: Time.now.to_i, body: "") + ts = timestamp.to_s + sig = "v0=" + OpenSSL::HMAC.hexdigest("SHA256", secret, "v0:#{ts}:#{body}") + { "X-Slack-Request-Timestamp" => ts, "X-Slack-Signature" => sig } + end +end