From fe944720a8e49bbe8c2f9e6c31e8f23efd3bcb3d Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 20 May 2026 13:38:33 +0530 Subject: [PATCH] refactor: tighten read-only access token scope enforcement --- app/controllers/api/base_controller.rb | 21 +++++++++++++++++++-- app/views/api/v1/models/_user.json.jbuilder | 5 ++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index bdc7575e0..5c54bd1f8 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -8,6 +8,13 @@ class Api::BaseController < ApplicationController api/v1/accounts/contacts#filter ].freeze + # GET-shaped endpoints that mutate state and must be blocked for read-only + # tokens despite the verb being "safe". Add any new write-on-GET endpoint + # here when it ships. + READ_ONLY_BLOCKED_GET_ACTIONS = %w[ + api/v1/accounts/callbacks#register_facebook_page + ].freeze + respond_to :json before_action :authenticate_access_token!, if: :authenticate_by_access_token? before_action :validate_bot_access_token!, if: :authenticate_by_access_token? @@ -22,13 +29,23 @@ class Api::BaseController < ApplicationController def enforce_read_only_token_scope return unless @access_token&.scope == 'read_only' - return if request.get? || request.head? || request.options? - return if READ_ONLY_POST_ALLOWLIST.include?("#{params[:controller]}##{params[:action]}") + return if read_only_token_action_allowed? render json: { error: 'This access token is read-only and cannot perform write operations.' }, status: :forbidden end + def read_only_token_action_allowed? + action_key = "#{params[:controller]}##{params[:action]}" + return READ_ONLY_POST_ALLOWLIST.include?(action_key) unless safe_http_method? + + READ_ONLY_BLOCKED_GET_ACTIONS.exclude?(action_key) + end + + def safe_http_method? + request.get? || request.head? || request.options? + end + def check_authorization(model = nil) model ||= controller_name.classify.constantize diff --git a/app/views/api/v1/models/_user.json.jbuilder b/app/views/api/v1/models/_user.json.jbuilder index 11ff6431e..0a16855d3 100644 --- a/app/views/api/v1/models/_user.json.jbuilder +++ b/app/views/api/v1/models/_user.json.jbuilder @@ -1,4 +1,7 @@ -json.access_token resource.access_token&.token +# Withhold the full-scope token from callers who authenticated with a +# read-only token — otherwise GET /api/v1/profile would let a read-only +# holder lift the full token and bypass the scope gate entirely. +json.access_token resource.access_token&.token unless @access_token&.scope == 'read_only' json.read_only_access_token resource.read_only_access_token&.token json.account_id resource.active_account_user&.account_id json.available_name resource.available_name