@@ -24,6 +24,9 @@ abstract class AssetLoader {
2424/// default used is RootBundleAssetLoader which uses flutter's assetloader
2525///
2626class RootBundleAssetLoader extends AssetLoader {
27+ // Place inside class RootBundleAssetLoader
28+ static const int _maxLinkedDepth = 32 ;
29+
2730 const RootBundleAssetLoader ();
2831
2932 String getLocalePath (String basePath, Locale locale) {
@@ -35,28 +38,64 @@ class RootBundleAssetLoader extends AssetLoader {
3538 }
3639
3740 Future <Map <String , dynamic >> _getLinkedTranslationFileDataFromBaseJson (
38- String basePath, Locale locale, Map <String , dynamic > baseJson,
39- {List <String > fileLoaded = const []}) async {
40- Map <String , dynamic > fullJson = Map <String , dynamic >.from (baseJson);
41+ String basePath,
42+ Locale locale,
43+ Map <String , dynamic > baseJson, {
44+ required Set <String > visited,
45+ required Map <String , Map <String , dynamic >> cache,
46+ int depth = 0 ,
47+ }) async {
48+ if (depth > _maxLinkedDepth) {
49+ throw StateError ('Maximum linked files depth ($_maxLinkedDepth ) exceeded for $locale at $basePath .' );
50+ }
51+
52+ final Map <String , dynamic > fullJson = Map <String , dynamic >.from (baseJson);
4153
42- for (var entry in baseJson.entries) {
43- var key = entry.key;
54+ for (final entry in baseJson.entries) {
55+ final key = entry.key;
4456 var value = entry.value;
4557
4658 if (value is String && value.startsWith (':/' )) {
47- String filePath = value.substring (2 );
59+ final rawPath = value.substring (2 ).trim ();
60+ // Normalize and reject traversal
61+ final normalizedPath = rawPath.replaceAll (RegExp (r'^[\\/]+' ), '' );
62+ if (normalizedPath.contains ('..' )) {
63+ throw FormatException ('Invalid linked file path "$rawPath " for key "$key ".' );
64+ }
65+ final linkedAssetPath = _getLinkedLocalePath (basePath, normalizedPath, locale);
4866
49- if (fileLoaded .contains (filePath )) {
50- throw Exception ( 'Circular reference detected: $ filePath is loaded multiple times ' );
67+ if (visited .contains (linkedAssetPath )) {
68+ throw StateError ( 'Cyclic linked files detected at "$ linkedAssetPath " (key: "$ key "). ' );
5169 }
5270
53- fileLoaded.add (filePath);
54- value = json.decode (await rootBundle.loadString (_getLinkedLocalePath (basePath, filePath, locale)));
71+ final Map <String , dynamic > linkedJson = cache[linkedAssetPath] ??
72+ (cache[linkedAssetPath] =
73+ (json.decode (await rootBundle.loadString (linkedAssetPath)) as Map <String , dynamic >));
74+
75+ visited.add (linkedAssetPath);
76+ try {
77+ value = await _getLinkedTranslationFileDataFromBaseJson (
78+ basePath,
79+ locale,
80+ linkedJson,
81+ visited: visited,
82+ cache: cache,
83+ depth: depth + 1 ,
84+ );
85+ } finally {
86+ visited.remove (linkedAssetPath);
87+ }
5588 }
5689
5790 if (value is Map <String , dynamic >) {
58- fullJson[key] =
59- await _getLinkedTranslationFileDataFromBaseJson (basePath, locale, value, fileLoaded: fileLoaded);
91+ fullJson[key] = await _getLinkedTranslationFileDataFromBaseJson (
92+ basePath,
93+ locale,
94+ value,
95+ visited: visited,
96+ cache: cache,
97+ depth: depth + 1 ,
98+ );
6099 }
61100 }
62101
@@ -69,6 +108,12 @@ class RootBundleAssetLoader extends AssetLoader {
69108 EasyLocalization .logger.debug ('Load asset from $path ' );
70109
71110 Map <String , dynamic > baseJson = json.decode (await rootBundle.loadString (localePath));
72- return await _getLinkedTranslationFileDataFromBaseJson (path, locale, baseJson);
111+ return await _getLinkedTranslationFileDataFromBaseJson (
112+ path,
113+ locale,
114+ baseJson,
115+ visited: < String > {},
116+ cache: < String , Map <String , dynamic >> {},
117+ );
73118 }
74119}
0 commit comments