33import com .google .gson .reflect .TypeToken ;
44import com .moandjiezana .toml .Toml ;
55import java .io .IOException ;
6+ import java .nio .charset .Charset ;
7+ import java .nio .charset .StandardCharsets ;
68import java .util .concurrent .TimeUnit ;
79import lombok .NonNull ;
810import okhttp3 .HttpUrl ;
11+ import okhttp3 .MediaType ;
912import okhttp3 .OkHttpClient ;
1013import okhttp3 .Request ;
1114import okhttp3 .Response ;
15+ import okhttp3 .ResponseBody ;
16+ import okio .Buffer ;
17+ import okio .BufferedSource ;
1218import org .stellar .sdk .exception .ConnectionErrorException ;
1319import org .stellar .sdk .exception .TooManyRequestsException ;
20+ import org .stellar .sdk .federation .exception .FederationResponseTooLargeException ;
1421import org .stellar .sdk .federation .exception .FederationServerInvalidException ;
1522import org .stellar .sdk .federation .exception .NoFederationServerException ;
1623import org .stellar .sdk .federation .exception .NotFoundException ;
1724import org .stellar .sdk .federation .exception .StellarTomlNotFoundInvalidException ;
25+ import org .stellar .sdk .federation .exception .StellarTomlTooLargeException ;
1826import org .stellar .sdk .requests .ResponseHandler ;
1927
2028/**
2331 * @see <a href="https://developers.stellar.org/docs/learn/glossary#federation">Federation</a>
2432 */
2533public class Federation {
34+ /**
35+ * Maximum allowed size for HTTP responses (100KB).
36+ *
37+ * <p>This limit prevents denial-of-service attacks where a malicious server could send an
38+ * infinite stream of data, causing OutOfMemoryError. Legitimate stellar.toml files and federation
39+ * responses should be well under this limit. If you need to handle larger responses, use the
40+ * constructor that accepts a custom OkHttpClient.
41+ */
42+ private static final long MAX_RESPONSE_SIZE = 100 * 1024 ;
43+
2644 private final OkHttpClient httpClient ;
2745
2846 /**
@@ -52,6 +70,9 @@ public Federation() {
5270 * @throws StellarTomlNotFoundInvalidException Stellar.toml file not found or invalid
5371 * @throws NoFederationServerException No federation server defined in stellar.toml file
5472 * @throws FederationServerInvalidException Federation server is invalid
73+ * @throws StellarTomlTooLargeException if the stellar.toml file exceeds maximum allowed size
74+ * @throws FederationResponseTooLargeException if the federation server response exceeds maximum
75+ * allowed size
5576 * @throws org.stellar.sdk.exception.BadRequestException if the request fails due to a bad request
5677 * (4xx)
5778 * @throws org.stellar.sdk.exception.BadResponseException if the request fails due to a bad
@@ -90,6 +111,9 @@ public FederationResponse resolveAddress(String address) {
90111 * @throws StellarTomlNotFoundInvalidException Stellar.toml file not found or invalid
91112 * @throws FederationServerInvalidException Federation server is invalid
92113 * @throws NoFederationServerException No federation server defined in stellar.toml file
114+ * @throws StellarTomlTooLargeException if the stellar.toml file exceeds maximum allowed size
115+ * @throws FederationResponseTooLargeException if the federation server response exceeds maximum
116+ * allowed size
93117 * @throws org.stellar.sdk.exception.BadRequestException if the request fails due to a bad request
94118 * (4xx)
95119 * @throws org.stellar.sdk.exception.BadResponseException if the request fails due to a bad
@@ -125,7 +149,17 @@ private FederationResponse resolve(String q, String domain, QueryType queryType)
125149 throw new NotFoundException ();
126150 }
127151
128- return responseHandler .handleResponse (response );
152+ if (response .body () == null ) {
153+ throw new ConnectionErrorException (new IOException ("Empty response body" ));
154+ }
155+
156+ // Limit response size to prevent DoS attacks
157+ String body = readResponseBodyWithLimit (response .body (), MAX_RESPONSE_SIZE );
158+ if (body == null ) {
159+ throw new FederationResponseTooLargeException (MAX_RESPONSE_SIZE );
160+ }
161+
162+ return responseHandler .handleResponse (response , body );
129163 } catch (IOException e ) {
130164 throw new ConnectionErrorException (e );
131165 }
@@ -158,7 +192,14 @@ private HttpUrl getFederationServerUri(@NonNull String domain) {
158192 if (response .body () == null ) {
159193 throw new StellarTomlNotFoundInvalidException ("Empty response body" );
160194 }
161- Toml stellarToml = new Toml ().read (response .body ().string ());
195+
196+ // Limit response size to prevent DoS attacks
197+ String body = readResponseBodyWithLimit (response .body (), MAX_RESPONSE_SIZE );
198+ if (body == null ) {
199+ throw new StellarTomlTooLargeException (MAX_RESPONSE_SIZE );
200+ }
201+
202+ Toml stellarToml = new Toml ().read (body );
162203 String federationServer = stellarToml .getString ("FEDERATION_SERVER" );
163204 if (federationServer == null || federationServer .isEmpty ()) {
164205 throw new NoFederationServerException ();
@@ -177,10 +218,64 @@ private static OkHttpClient createHttpClient() {
177218 return new OkHttpClient .Builder ()
178219 .connectTimeout (10 , TimeUnit .SECONDS )
179220 .readTimeout (30 , TimeUnit .SECONDS )
221+ .callTimeout (60 , TimeUnit .SECONDS )
180222 .retryOnConnectionFailure (false )
181223 .build ();
182224 }
183225
226+ /** UTF-8 BOM (Byte Order Mark) character. */
227+ private static final char UTF8_BOM = '\uFEFF' ;
228+
229+ /**
230+ * Reads the response body with a size limit to prevent DoS attacks.
231+ *
232+ * <p>This method reads directly from the byte stream, ignoring Content-Length headers, to prevent
233+ * attacks where a malicious server sends more data than declared. It respects the charset
234+ * specified in the Content-Type header, defaulting to UTF-8 if not specified. UTF-8 BOM is
235+ * automatically stripped if present.
236+ *
237+ * @param responseBody The response body to read from
238+ * @param maxSize Maximum number of bytes to read
239+ * @return The response body as a string, or null if the response exceeds maxSize
240+ * @throws IOException If an I/O error occurs
241+ */
242+ private static String readResponseBodyWithLimit (ResponseBody responseBody , long maxSize )
243+ throws IOException {
244+ // Get charset from Content-Type, default to UTF-8
245+ Charset charset = StandardCharsets .UTF_8 ;
246+ MediaType contentType = responseBody .contentType ();
247+ if (contentType != null ) {
248+ Charset contentTypeCharset = contentType .charset ();
249+ if (contentTypeCharset != null ) {
250+ charset = contentTypeCharset ;
251+ }
252+ }
253+
254+ BufferedSource source = responseBody .source ();
255+ Buffer buffer = new Buffer ();
256+ long totalRead = 0 ;
257+
258+ while (!source .exhausted ()) {
259+ long read = source .read (buffer , 8192 );
260+ if (read == -1 ) {
261+ break ;
262+ }
263+ totalRead += read ;
264+ if (totalRead > maxSize ) {
265+ return null ;
266+ }
267+ }
268+
269+ String result = buffer .readString (charset );
270+
271+ // Strip UTF-8 BOM if present (consistent with OkHttp ResponseBody.string() behavior)
272+ if (!result .isEmpty () && result .charAt (0 ) == UTF8_BOM ) {
273+ result = result .substring (1 );
274+ }
275+
276+ return result ;
277+ }
278+
184279 private enum QueryType {
185280 NAME ("name" ),
186281 ID ("id" );
0 commit comments