Правильно ли этот (небольшой) фрагмент кода предотвращает атаки с синхронизацией ключей API? (RoR)

#ruby-on-rails #api

#ruby-on-rails #API

Вопрос:

Я реализую аутентификацию / авторизацию КЛЮЧА API для проекта API, который я создаю. Я использую authenticate_or_request_with_http_token вызов метода и защищаю от атак по времени. Я впервые прочитал о временных атаках и попытался реализовать эту безопасную защиту в моем ApplicationController . Я считаю, что механизм выполнен правильно, но хотел бы получить подтверждение. Вот фрагмент кода.

 before_action :authenticate_request

def authenticate_request
  authenticate_or_request_with_http_token do |token, options|
    api_key = ApiKey.find_by(access_token: token)
    secret_key = ApiKey.find(api_key.id).secret_key
    api_key.present? amp;amp; (api_key.active != false) amp;amp; secure_compare_with_hashing(api_key.secret_key, secret_key)
  end
end

def secure_compare_with_hashing(key1, key2)
  ActiveSupport::SecurityUtils.secure_compare(key1, key2)
end
  

В принципе, я 1). Поиск ключа Api по токену, представленному в заголовке, 2). Затем я нахожу использование идентификатора api_key для извлечения секретного ключа. 3). Я сравниваю секретный ключ обоих значений для сравнения постоянной времени.

Если я не ошибаюсь, я считаю, что это должно обеспечить успешное смягчение временных атак. Я прав в этом?

====== обновление ======

Для контекста я использую этот ресурс для решения проблемы предотвращения атак с ошибкой синхронизации. Вот небольшая цитата из онлайн-книги.

Способ, которым мы реализовали ключ API, уязвим для атак по времени. Вот почему многие веб-API поставляются с ключом доступа и секретным ключом. Ключ доступа можно использовать для поиска записи в базе данных перед выполнением безопасного сравнения секретных ключей.

Чтобы проверить, действителен ли ключ API безопасным способом, нам нужно получить запись базы данных без использования фактического токена. Действительно, выполнение SQL-запроса типа ApiKey.where(ключ: ‘abc’) уязвимо для атак с синхронизацией.

Чтобы избежать этого, мы можем попросить клиента предоставить нам другой способ поиска записи, например, отправив идентификатор ключа. В итоге мы получим api_key=1:abc вместо api_key=abc, где 1 — идентификатор ключа API.

Для реального приложения мы должны были создать новое поле для сохранения ключа доступа вместо использования идентификатора.

Вот код, который я пытаюсь адаптировать.

 module Authentication
  include ActiveSupport::SecurityUtils
  # Hidden Code

  included # Hidden Code

  private

  def validate_auth_scheme # Hidden Code
  def unauthorized! # Hidden Code
  def authorization_request # Hidden Code

  def credentials
    @credentials ||= Hash[authorization_request.scan(/(w )[:=] ?"?([w|:] )"?/)]
  end

  def api_key
    @api_key ||= compute_api_key
  end

  def compute_api_key
    return nil if credentials['api_key'].blank?

    id, key = credentials['api_key'].split(':')
    api_key = id amp;amp; key amp;amp; ApiKey.activated.find_by(id: id)

    return api_key if api_key amp;amp; secure_compare_with_hashing(api_key.key, key)
  end

  def secure_compare_with_hashing(a, b)
    secure_compare(Digest::SHA1.hexdigest(a), Digest::SHA1.hexdigest(b))
  end

end
  

Мой код неправильно следует этой процедуре? Игнорируя проблемный случай, когда мой код не возвращает ключ, имеет ли смысл сравнение secret_key ?

Например,

 api_key = ApiKey.find_by(access_token: token) #Timing attack :(
  

Я получаю секретный ключ, используя api_key, который я нашел.

 secret_key = ApiKey.find(api_key.id).secret_key
  

Атака по времени обойдена.

 secure_compare_with_hashing(api_key.secret_key, secret_key)
  

Ответ №1:

secure_compare предотвращает временные атаки, преобразуя ваши строки в одинаково длинные хэши, так что все должно быть в порядке.

Тем не менее, мне интересно, что делает предыдущий код.

Этот фрагмент не извлекает одно и то же ApiKey дважды? Также в вашей реализации есть ошибка, когда токен не существует.

 api_key = ApiKey.find_by(access_token: token)
secret_key = ApiKey.find(api_key.id).secret_key # this will crash if token does not exist

secure_compare_with_hashing(api_key.secret_key, secret_key) # is always true, no?
  

Кроме того, это приведет к досрочному возврату. Хотя невозможно угадать весь пароль, все равно можно получить информацию, если, например, существует api_key .

 api_key.present? amp;amp; (api_key.active != false) 
  

https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/activesupport/lib/active_support/security_utils.rb#L27

## Редактировать

По сравнению с предоставленным вами руководством вы сделали именно то, что не должны делать.

 def authenticate_request
  authenticate_or_request_with_http_token do |token, options|
    api_key = ApiKey.find_by(access_token: token) # <- TIMING ATTACK ON DB
    # ...
  end
end
  

Вместо этого вам следует указать идентификатор пользователя / ключа в параметрах и использовать его для извлечения ключа из базы данных. Я также изменил ваше условие, чтобы всегда выполнять безопасное сравнение и выполнять активную проверку в конце.

 def authenticate_request
  authenticate_or_request_with_http_token do |token, options|
    api_key = ApiKey.find_by(id: options[:key_id])

   secure_compare_with_hashing(api_keyamp;.secret_key.to_s, token) amp;amp;
     api_key.active
  end
end
  

Комментарии:

1. Привет, Кристиан, спасибо, что ответил на мой вопрос, не могли бы вы взглянуть на мое обновление? Помимо ошибки, из-за которой ключ не найден или отключен, я обошел атаку по времени? Я беру ключ, указанный в токене, ищу секретный ключ, а затем сравниваю секретный ключ. Я заметил, что безопасное сравнение всегда возвращает true, что показалось мне странным в примере, который я адаптирую.

2. В качестве еще одного обновления, по сути, я хочу, чтобы это было идеально изложено в книге. <«Вот почему многие веб-API поставляются с ключом доступа и секретным ключом. Ключ доступа можно использовать для поиска записи в базе данных перед выполнением безопасного сравнения секретных ключей «>

3. Вы в основном просто c amp; p фрагмент из книги, так что да, похоже, он не уязвим для временных атак. Если это ответило на ваш вопрос, пожалуйста, подумайте о принятии ответа.

4. Обновил мой первоначальный ответ.