ES /docs

ArgumentError: invalid byte sequence in UTF-8

RCA: ArgumentError: invalid byte sequence in UTF-8

Overview#

What Happened#

migration-service (Rails 7.1 API) 가 인터넷 스캐너의 악성 요청을 받을 때마다 ArgumentError: invalid byte sequence in UTF-8 로 HTTP 500 을 반환한다. 요청은 전부 동일한 PHP RCE 스캔 프로브(POST /hello.world?%ADd+allow_url_include%3d1+..., User-Agent libredtail-http = RedTail 봇넷)이며, URL 쿼리스트링에 포함된 잘못된 바이트(%AD = 0xAD)가 Rails 로그 파라미터 필터링 단계에서 UTF-8 정규식 매칭을 깨뜨린다. 애플리케이션 코드가 실행되기 전 미들웨어 단계에서 터지므로 컨트롤러의 rescue_from 이 잡지 못하고, 14일간 dev·production 양쪽에서 113건 발생했다.

Quick Facts#

Field Value
exception.class ArgumentError
exception.message invalid byte sequence in UTF-8
top_frame activesupport-7.1.4/lib/active_support/parameter_filter.rb:140:in 'match?'
runtime Ruby 2.7.0 / Rails 7.1.4 (activesupport, actionpack)
deploy dev.azure.com/cupix/.../cupixworks (migration-service)
env dev + production (us-west-2), 요청 원본은 외부 IP 35.165.238.184

Affected Teams#

Team / Domain Error Count Impact
cupixworks-migration-service-api 113 (14d) / 144 (누적) 사용자 영향 없음 — 요청은 전부 외부 스캐너 프로브이며 실제 API 엔드포인트(/api/v1/migrations)와 무관

Timeline#

  1. 2024-07-19 10:58 KST — 최초 발생 (first_seen)
  2. 2026-07-30 ~ 2026-08-04 — 14일 창에서 113건, 하루 수 건씩 dev·production 에 산발 (전부 동일 RedTail 프로브)
  3. 2026-08-04 16:17 KST — 최근 발생 (last_seen), Representative 와 동일 메시지
  4. 2026-08-04 — RCA 수행, external scanner noise 로 판정

Error Log#

Datadog Logs

text
invalid byte sequence in UTF-8

Impact#

  • Service: cupixworks-migration-service-api
  • 발생 횟수: 144 (누적), 113 (최근 14일)
  • 최초 발생: 2024-07-19 10:58 KST
  • 최근 발생: 2026-08-04 16:17 KST

Root Cause Summary#

외부 인터넷 봇넷 스캐너(RedTail, UA libredtail-http)가 PHP RCE 취약점을 노리고 POST /hello.world?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input 를 반복 전송한다. 쿼리스트링의 %AD 는 UTF-8 로 유효하지 않은 바이트(0xAD)이며, Rails 가 요청을 로그에 남기기 위해 ActionDispatch::Http::FilterParameters#filtered_parameters 로 파라미터를 필터링할 때 ActiveSupport::ParameterFilter#value_for_key 가 이 깨진 문자열 key 에 대해 key.match?(filter_regexp) 를 호출한다. Ruby 의 String#match? 는 invalid encoding 문자열에 대해 ArgumentError: invalid byte sequence in UTF-8 를 raise 한다. 이 예외는 컨트롤러 액션 진입 이전, ActionController::Metal::Instrumentation#process_action 의 로깅 계층에서 발생하므로 ServerErrorControllerrescue_from StandardError(server_error_controller.rb:8) 범위 밖이며, Rack 미들웨어까지 전파되어 HTTP 500 이 반환된다. 즉 tesla/migration-service 애플리케이션 로직의 결함이 아니라, malformed input + Rails 로깅 계층의 알려진 동작이 결합된 것이고 트래픽 자체가 악성 스캐너 노이즈다.

Technical Analysis#

Code Path#

  • Entry point (요청): 외부 스캐너 → POST /hello.world?%ADd+... → migration-service Rails
  • Route: catch-all 이 존재하지만 예외는 그 이전에 발생
config/routes.rb:24ruby
  match '*path', :to => 'application#routing_error', via: :all
  • 로깅 파라미터 필터 설정 (symbol key → 컴파일된 regexp):
config/initializers/filter_parameter_logging.rb:6-8ruby
Rails.application.config.filter_parameters += [
  :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
]
  • Failure point: activesupport-7.1.4/lib/active_support/parameter_filter.rb:140value_for_key 내부에서 각 필터 key 를 요청 파라미터 key 와 match? 로 비교하다가, 깨진 UTF-8 파라미터 key 에서 raise. Datadog span error.stack 원문:
activesupport-7.1.4/lib/active_support/parameter_filter.rb:140text
.../active_support/parameter_filter.rb:140:in `match?': invalid byte sequence in UTF-8 (ArgumentError)
	from .../active_support/parameter_filter.rb:140:in `block in value_for_key'
	from .../active_support/parameter_filter.rb:140:in `any?'
	from .../active_support/parameter_filter.rb:140:in `value_for_key'
	from .../active_support/parameter_filter.rb:129:in `block in call'
	from .../active_support/parameter_filter.rb:84:in `filter'
	from .../action_dispatch/http/filter_parameters.rb:31:in `filtered_parameters'
	from .../action_controller/metal/instrumentation.rb:64:in `process_action'
  • 기대 동작 vs 실제 동작: 정상 요청이라면 파라미터 필터가 민감 값을 [FILTERED] 로 치환한 뒤 로그를 남기고 요청을 계속 처리한다. 실제로는 파라미터 key 가 invalid UTF-8 이라 match? 가 raise 하고, 이 예외가 로깅(instrumentation) 계층에서 터져 컨트롤러의 rescue_from 이 개입하기 전에 500 으로 귀결된다.
  • rescue_from 이 잡지 못하는 이유: 앱의 500 핸들러는 컨트롤러 액션 실행 중 예외만 포착한다.
app/controllers/concerns/server_error_controller.rb:8,21-24ruby
    rescue_from StandardError, with: :server_500_error
    # ...
    def server_500_error(exception)
      Cupix::Logger.error("[Migration] Server 500 error - #{exception}", class: self.class.name, function: __method__)
      raise_error(500, exception)
    end

ArgumentError < StandardError 이지만 예외가 process_actionfiltered_parameters 로깅 단계(액션 dispatch 상위)에서 발생하므로 이 핸들러 범위 밖이다. 증거: 14일 로그 검색에서 [Migration] Server 500 error 로그가 이 메시지에 대해 0건 — 즉 server_500_error 가 실행된 적이 없다.

Log Evidence#

status:error 로그 검색 (0건 — 앱 로거를 거치지 않음, APM span 만 존재):

text
service:cupixworks-migration-service-api "invalid byte sequence"   → 0 logs (now-14d)
service:cupixworks-migration-service-api "ArgumentError"           → 0 logs (now-14d)

APM span 검색 (실제 발생 확인):

text
service:cupixworks-migration-service-api status:error @error.type:ArgumentError

14일 창에서 113건, 전부 동일. 대표 span 핵심 필드:

json
{
  "error": {
    "type": "ArgumentError",
    "message": "invalid byte sequence in UTF-8",
    "file": ".../active_support/parameter_filter.rb"
  },
  "http": {
    "method": "POST",
    "status_code": "500",
    "url": "/hello.world?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input",
    "url_details": { "path": "/hello.world" },
    "useragent": "libredtail-http",
    "base_url": "https://35.165.238.184"
  },
  "appsec": {
    "attack_attempt": ["attack_tool"],
    "bots": { "qualification": "harmful", "tool_name": "redtail" },
    "triggers": [{ "rule": { "id": "ua0-600-69x", "name": "RedTail" } }]
  },
  "env": "dev"
}
  • 모든 113건의 URL/UA 가 동일: POST /hello.world?%ADd+..., libredtail-http. env 는 dev·production 혼재, base_url 은 공용 IP 35.165.238.184.
  • Datadog AppSec 가 이 요청을 harmful bot / attack_tool (WAF rule ua0-600-69x "RedTail") 로 명시 태깅 → 정상 API 트래픽이 아니라 인터넷 스캐너임이 확정.
  • last_seen span 2026-08-04T07:17:41.762Z 가 클러스터 last_seen (2026-08-04T07:17:41.764Z) 과 정확히 일치 → Representative 는 stale 아님, 현재 발생 메시지와 동일.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 외부 스캐너의 malformed URL(%AD invalid UTF-8)이 Rails 로그 파라미터 필터(ParameterFilter#match?)를 깨뜨려 미들웨어 단계에서 500 span stack parameter_filter.rb:140 match?; URL %ADd...; UA libredtail-http; AppSec tool_name:redtail; 113/113 동일 요청 없음 Confirmed
H2 migration-service 애플리케이션 코드(컨트롤러/모델)의 인코딩 처리 결함 앱 코드에 force_encoding/match/regex 처리 부재; 예외 stack 전부 gem(activesupport/actionpack) 내부; /hello.world 는 앱 라우트 아님 Rejected
H3 Representative Error 가 stale (다른 변형이 현재 발생 중) last_seen span 타임스탬프가 클러스터 last_seen 과 일치, 메시지·URL·UA 전부 동일 Rejected
H4 rescue_from StandardError (server_500_error) 로 잡혀야 하는데 못 잡는 버그 예외가 500 으로 귀결 예외 발생 지점이 instrumentation#process_action 로깅 단계(컨트롤러 액션 상위)라 rescue_from 범위 밖 — 설계상 정상; [Migration] Server 500 error 로그 0건이 이를 확인 Rejected

Fix Recommendation#

이 이슈는 noise 다 (외부 악성 스캐너 트래픽 + Rails 로깅 계층의 알려진 동작). 애플리케이션 로직 버그가 아니므로 코드 결함 수정 대상이 아니다. 다만 알람 노이즈 감소와 방어 관점의 개선은 선택적으로 가능하다.

즉시 조치 (Critical)#

  • 코드 변경 불필요. Error Tracking 에서 이 이슈를 IGNORE 처리하여 알람 노이즈를 제거하는 것을 권장.

단기 개선 (1주 이내)#

  • (선택) 인프라 계층에서 악성 스캐너 차단: 이미 Datadog AppSec 가 libredtail-http 를 harmful bot 으로 탐지하고 있으므로, ALB/CloudFront WAF 에서 해당 UA·패턴(/hello.world, php://input)을 block 하면 요청이 앱에 도달하기 전에 차단된다. 이는 migration-service 만의 문제가 아니라 공용 엔드포인트 공통 사안이므로 인프라 팀 조율 대상.
  • (선택) invalid-encoding 요청을 앱 진입 전에 정규화/거부하는 Rack 미들웨어(예: Rack::UTF8Sanitizer 계열) 도입 시, 이 500 이 로깅 단계에서 터지지 않고 4xx 로 정상 매핑된다. 단 migration-service 는 내부 서비스이므로 우선순위 낮음.

장기 개선 (재발 방지)#

  • 공용 노출 서비스 전반에 대해 WAF 봇 차단 정책을 표준화하고, invalid-encoding 요청에 대한 공통 Rack sanitizer 를 베이스 이미지/공통 gem 레벨에서 적용.

Monitoring#

  • 스캐너 프로브 500 추이 (모두 동일 UA/URL 이므로 추이만 관찰):
text
sum:trace.rack.request.errors{service:cupixworks-migration-service-api,http.status_code:500}.as_count()
  • migration-service 전체 5xx 비율 (스캐너 노이즈 vs 실제 API 오류 분리 확인용):
text
sum:trace.rack.request.errors{service:cupixworks-migration-service-api}.as_count()

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (코드 변경 없음 — IGNORE 처리 + 선택적 인프라 WAF 조율)

Noise Verdict#

noise — 외부 RedTail 봇넷 스캐너의 잘못된 UTF-8 URL 프로브가 Rails 로그 파라미터 필터를 깨뜨려 500 이 날 뿐, 애플리케이션 코드 결함이 아니며 실제 사용자 영향이 없다.