diff --git a/src/config.cpp b/src/config.cpp index bef34b9e74..ae93fcba70 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -1399,6 +1399,43 @@ void ClearProviders(Config& config) { config.model.decoder.session_options.providers.clear(); } +// Escape a string for safe embedding inside a JSON string literal. Prevents JSON +// injection when caller-supplied values are concatenated into a JSON document that +// will subsequently be parsed (e.g. in SetProviderOption below). Handles the +// mandatory JSON escapes: quote, backslash, and the C0 control-character shortcuts +// that the local JSON parser understands (\b \f \n \r \t). +// +// Other C0 control characters (< 0x20) have no shortcut and would require a +// \uXXXX escape, which the local JSON parser in src/json.cpp does not support +// (it throws "Unsupported uXXXX code used"). Since provider option names and +// values are configuration strings that are not expected to contain raw +// control characters, reject them here with a clear error rather than +// producing JSON that the parser cannot consume. +static std::string EscapeJsonString(std::string_view s) { + std::string result; + result.reserve(s.size()); + for (char c : s) { + switch (c) { + case '"': result += "\\\""; break; + case '\\': result += "\\\\"; break; + case '\b': result += "\\b"; break; + case '\f': result += "\\f"; break; + case '\n': result += "\\n"; break; + case '\r': result += "\\r"; break; + case '\t': result += "\\t"; break; + default: + if (static_cast(c) < 0x20) { + throw std::runtime_error( + "Unsupported control character in provider option string (code " + + std::to_string(static_cast(c)) + ")"); + } + result += c; + break; + } + } + return result; +} + void SetProviderOption(Config& config, std::string_view provider_name, std::string_view option_name, std::string_view option_value) { // Normalize the provider name once auto normalized_provider = NormalizeProviderName(provider_name); @@ -1421,10 +1458,14 @@ void SetProviderOption(Config& config, std::string_view provider_name, std::stri } } + // JSON-escape all caller-supplied string fragments before concatenating them into the + // JSON document. Without escaping, quote/backslash characters in provider_name, + // option_name, or option_value would let a caller inject arbitrary JSON structure + // (sibling keys, new provider entries, etc.) into the parsed configuration. std::ostringstream json; - json << R"({")" << provider_name << R"(":{)"; + json << R"({")" << EscapeJsonString(provider_name) << R"(":{)"; if (!option_name.empty()) { - json << R"(")" << option_name << R"(":")" << option_value << R"(")"; + json << R"(")" << EscapeJsonString(option_name) << R"(":")" << EscapeJsonString(option_value) << R"(")"; } json << R"(}})";