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#
- 2024-07-19 10:58 KST — 최초 발생 (first_seen)
- 2026-07-30 ~ 2026-08-04 — 14일 창에서 113건, 하루 수 건씩 dev·production 에 산발 (전부 동일 RedTail 프로브)
- 2026-08-04 16:17 KST — 최근 발생 (last_seen), Representative 와 동일 메시지
- 2026-08-04 — RCA 수행, external scanner noise 로 판정
Error Log#
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 의 로깅 계층에서 발생하므로 ServerErrorController 의 rescue_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 이 존재하지만 예외는 그 이전에 발생
match '*path', :to => 'application#routing_error', via: :all
- 로깅 파라미터 필터 설정 (symbol key → 컴파일된 regexp):
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:140—value_for_key내부에서 각 필터 key 를 요청 파라미터 key 와match?로 비교하다가, 깨진 UTF-8 파라미터 key 에서 raise. Datadog spanerror.stack원문:
.../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 핸들러는 컨트롤러 액션 실행 중 예외만 포착한다.
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_action 의 filtered_parameters 로깅 단계(액션 dispatch 상위)에서 발생하므로 이 핸들러 범위 밖이다. 증거: 14일 로그 검색에서 [Migration] Server 500 error 로그가 이 메시지에 대해 0건 — 즉 server_500_error 가 실행된 적이 없다.
Log Evidence#
status:error 로그 검색 (0건 — 앱 로거를 거치지 않음, APM span 만 존재):
service:cupixworks-migration-service-api "invalid byte sequence" → 0 logs (now-14d)
service:cupixworks-migration-service-api "ArgumentError" → 0 logs (now-14d)
APM span 검색 (실제 발생 확인):
service:cupixworks-migration-service-api status:error @error.type:ArgumentError
14일 창에서 113건, 전부 동일. 대표 span 핵심 필드:
{
"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 은 공용 IP35.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 이므로 추이만 관찰):
sum:trace.rack.request.errors{service:cupixworks-migration-service-api,http.status_code:500}.as_count()
- migration-service 전체 5xx 비율 (스캐너 노이즈 vs 실제 API 오류 분리 확인용):
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 이 날 뿐, 애플리케이션 코드 결함이 아니며 실제 사용자 영향이 없다.