Localize version 0.30.0
[0.30.0] — May 12th, 2026
Security
-
Localize.LanguageTag.parse/1no longer callsString.to_atom/1on raw parser output, closing an atom-table-exhaustion DOS vector on untrusted locale inputs. Atomisation is now gated behind the CLDR validity sets after alias resolution, and unrecognised language/script/territory subtags returnLocalize.InvalidSubtagError. -
Localize.Locale.to_locale_id/1renamed toLocalize.Locale.cldr_locale_id_from/1and now returns{:ok, atom()} | {:error, Exception.t()}, gating atom creation behindLocalize.validate_locale/1and closing a second atom-table-exhaustion vector on locale inputs. -
Localize.Currency.validate_currency/1,territory_currencies/1,current_currency_for_territory/1, and the binary-code branch ofcurrencies_for_locale/3's filter no longer atomise input before checking validity. Unknown currency or territory binaries are rejected viaHelpers.existing_atom/1and never grow the atom table. -
Localize.Script.display_name/2andLocalize.Unit.Formatterno longer atomise binary input before checking validity. Unknown script codes returnLocalize.UnknownScriptErrorwithout growing the atom table; the unit formatter's currency atomisation is gated as defence-in-depth behind the upstreamLocalize.Unit.validate_currency_codes/1check. -
MF2
:listfunction and unit parser no longer atomise user-controlled binaries. The:listfunction's binarystyle=fallthrough now sets a sentinel atom that surfaces as anInvalidValueErrorinLocalize.List, and SI prefix names are resolved through a compile-time lookup map (Localize.Unit.Data.si_prefix_atom/1) rather thanString.to_atom/1at parse time. -
Localize.Number.System(system_name_from/2,number_system_digits/1,to_system/2),Localize.Number.Symbol.number_symbols_for/2, and the datetime-formatter'stime_preferences_for/1no longer atomise user-supplied binary number-system or locale names before validation. Lookups go throughHelpers.existing_atom/1against pre-atomised CLDR data sets. -
Closed additional Atom DOS vectors in
Localize.Locale.LocaleDisplay.display_name/2(now routes throughcldr_locale_id_from/1),Localize.Territory.Subdivision.display_name/2, the-u-co-and-u-kr-extension parsers inLocalize.Collation.Options, and the redundantString.to_atom(to_string(...))round-trip in plural-rule fallback. -
Closed three further atom-DOS sites called out by the security audit's findings 1.4 and 1.5:
LocaleDisplay.U.find_exemplar_city/2(-u-tz-IANA region/city splitter),LocaleDisplay.T.to_atom_safe/1(-t-extension subtag normalisation), andGettext.Interpolation.safe_to_atom/1(missing-binding name reporting). All three previously fell through toString.to_atom/1on a miss, which defeated the helper's name; they now return the original binary unchanged when no atom exists. -
Locale cache files and downloaded ETFs are decoded with
:erlang.binary_to_term(_, [:safe]). Closes a node-crash vector for any deployment with:locale_cache_dirset to a writable directory: a malicious or corrupted cache file can no longer resurrect arbitrary atoms, funs, or refs. Failed safe decodes surface asLocaleNotFoundInCacheError(orLocaleDownloadErrorfor the download path) and the file is treated as stale. -
Public parser entry points now reject oversized input before invoking the grammar, capping the parser's CPU exposure on hostile input. Defaults are 256 bytes for
Localize.LanguageTag.parse/1andLocalize.Unit.Parser.parse/1, 64 KB forLocalize.Message.Parser.parse/1, and 1 KB forLocalize.Number.Parser.parse/2. Each cap is configurable via app env (:max_locale_id_bytes,:max_message_bytes,:max_unit_bytes,:max_number_bytes).Number.Parser.parse/2additionally rejectsDecimalresults whose exponent magnitude exceeds:max_decimal_exponent(default ±100) so downstream multiplication or formatting cannot materialise huge mantissas. -
Localize.FormatCacheETS table switched from:publicto:protected; writes are routed through the cache GenServer. The size cap (:format_cache_max_entries, default 2 000) is now enforced synchronously on each insert rather than by a 10-second sweeper, replacing the previous biased-random eviction that could leave the cache oversized. NewLocalize.FormatCache.clear/0andsize/0helpers added for tests and maintenance. -
NIF (ICU bindings) hardened. All NIF entries except
nif_plural_rulenow run on the dirty CPU scheduler pool (ERL_NIF_DIRTY_JOB_CPU_BOUND); the collator pool is sized forschedulers + dirty_cpu_schedulersandreserve_collrefuses overflow rather than reading past the array end. The reorder-codes branch capsnumCodesat 256 and checksenif_allocbefore use; everystd::stoll/std::stod/std::stoiis wrapped intry/catchso out-of-range C++ exceptions cannot unwind through the NIF boundary; the hand-rolled JSON arg parser guards each access afterskip_ws; per-call input lengths are capped at the NIF boundary (MAX_MF2_BYTES = 64 KB,MAX_COLLATION_BYTES = 1 MB,MAX_NUMBER_STR_BYTES = 1 KB). -
Localize.Unit.CustomRegistry.load_file/1now refuses to evaluate the file in:prod(or any environment without a loadedMixmodule — typical for releases) unlessconfig :localize, :allow_runtime_unit_files, trueis explicitly set. Outside:prodthe function works as before. The flag exists so an unintended feature switch in production cannot accidentally surface arbitrary code execution viaCode.eval_file/1. -
:localize_locale_cacheETS table switched from:publicto:protected, owned byLocalize.Locale.Loader. Writes are routed through the owner viacast(so the hot validate path doesn't block and writes triggered from inside the owner's ownhandle_callcannot deadlock). Reads remain direct ETS lookups. Combined with the format cache fix above, both ETS caches are now:protectedagainst multi-tenant or other-library interference. -
Localize.Utils.Http.get/2andget_with_headers/2now reject responses larger than 50 MB by default (configurable via:max_http_body_bytesapp env or per-call:max_body_bytesoption). Without the cap a malicious or compromised CDN could feed a multi-gigabyte response and OOM the BEAM. Oversized responses log an error and return{:error, :response_too_large}. Additionally, when peer certificate verification has been disabled (viaLOCALIZE_UNSAFE_HTTPS), a one-timeLogger.warningis emitted so a misconfigured production deployment cannot silently downgrade TLS without leaving an audit trail.