java-topology/docs/tickets/sinatra-0001-content-type-add-charset-array-scan.md

2.5 KiB

sinatra-0001: CWE-407 — content_type iterates add_charset Array on every response

Field Value
ID sinatra-0001
Project sinatra/sinatra
Severity MEDIUM
Status OPEN
CWE CWE-407 (Algorithmic Complexity)
Found 2026-03-27

Location

lib/sinatra/base.rb:392

def content_type(type = nil, params = {})
  ...
  unless params.include?(:charset) || settings.add_charset.all? { |p| !(p === mime_type) }
    params[:charset] = params.delete('charset') || settings.default_encoding
  end

Root Cause

settings.add_charset is an Array set at line 1950-1951:

set :add_charset, %w[javascript xml xhtml+xml].map { |t| "application/#{t}" }
settings.add_charset << %r{^text/}

This Array contains both strings and Regexps. For each call to content_type, the code calls Array#all? iterating every element and evaluating p === mime_type (which for Regexp is a match). This is O(k) per response where k = length of add_charset.

content_type is called on virtually every response (Sinatra sets it in helpers, in send_file, in template rendering, etc.). With the default configuration k=4, but users can extend the array to arbitrary length.

Hot Path

dispatch! → (template render / json / etc.) → content_type → O(k) scan. Called once per request at minimum, potentially multiple times per request.

Fix

Since add_charset supports both String equality and Regexp match via ===, a pure Set won't help here (Regexp === can't be O(1)-indexed). The practical fix is to split the list into a Set<String> for exact matches and a separate Array<Regexp> for pattern matches, checking the Set first (O(1)) and only falling through to Regexp scan on miss:

# In configure block or as a helper:
add_charset_strings = Set.new(settings.add_charset.select { |p| p.is_a?(String) })
add_charset_patterns = settings.add_charset.select { |p| p.is_a?(Regexp) }

unless params.include?(:charset) ||
       (!add_charset_strings.include?(mime_type) &&
        add_charset_patterns.none? { |p| p === mime_type })
  params[:charset] = ...
end

For the common case (all strings, short list) this is micro-opt. For the Regexp case this is unchanged. The bigger win is freezing the set at app startup rather than re-scanning per request.

Patch

defects/sinatra/patch/sinatra-0001-content-type-add-charset-set.patch

Benchmark

defects/sinatra/unit/SinatraTest.java