@@ -96,13 +96,28 @@ static inline bool EndsWith(const std::string& value, const std::string& suffix)
9696 return std::equal (suffix.rbegin (), suffix.rend (), value.rbegin ());
9797}
9898
99- // Dev-only HTTP ESM loader helpers
99+ // HTTP ESM loader helpers
100100
101101static inline bool StartsWith (const std::string& s, const char * prefix) {
102102 size_t n = strlen (prefix);
103103 return s.size () >= n && s.compare (0 , n, prefix) == 0 ;
104104}
105105
106+ // Security gate
107+ // In debug mode, all URLs are allowed. In production, checks security.allowRemoteModules and security.remoteModuleAllowlist
108+ static inline bool IsHttpUrlAllowedForLoading (const std::string& url) {
109+ return IsRemoteUrlAllowed (url);
110+ }
111+
112+ // Helper to create a security error message for blocked remote modules
113+ static std::string GetRemoteModuleBlockedMessage (const std::string& url) {
114+ if (!IsRemoteModulesAllowed ()) {
115+ return " Remote ES modules are not allowed in production. URL: " + url +
116+ " . Enable via security.allowRemoteModules in nativescript.config" ;
117+ }
118+ return " Remote URL not in security.remoteModuleAllowlist: " + url;
119+ }
120+
106121
107122static v8::MaybeLocal<v8::Module> CompileModuleFromSource (v8::Isolate* isolate, v8::Local<v8::Context> context,
108123 const std::string& code, const std::string& urlStr) {
@@ -725,8 +740,18 @@ static bool IsDocumentsPath(const std::string& path) {
725740
726741 // ── Early absolute-HTTP fast path ─────────────────────────────
727742 // If the specifier itself is an absolute HTTP(S) URL, resolve it immediately via
728- // the HTTP dev loader and return before any filesystem candidate logic runs.
743+ // the HTTP loader and return before any filesystem candidate logic runs.
729744 if (StartsWith (spec, " http://" ) || StartsWith (spec, " https://" )) {
745+ // Security check: block if remote modules not allowed
746+ if (!IsHttpUrlAllowedForLoading (spec)) {
747+ std::string msg = GetRemoteModuleBlockedMessage (spec);
748+ if (IsScriptLoadingLogEnabled ()) {
749+ Log (@" [resolver][security] blocked remote module: %s " , spec.c_str ());
750+ }
751+ isolate->ThrowException (v8::Exception::Error (tns::ToV8String (isolate, msg.c_str ())));
752+ return v8::MaybeLocal<v8::Module>();
753+ }
754+
730755 std::string key = CanonicalizeHttpUrlKey (spec);
731756 // Added instrumentation for unified phase logging
732757 Log (@" [http-esm][compile][begin] %s " , key.c_str ());
@@ -825,6 +850,7 @@ static bool IsDocumentsPath(const std::string& path) {
825850 // ("./" or "../") or root-absolute ("/") specifiers should resolve against the
826851 // referrer's URL, not the local filesystem. Mirror browser behavior by using NSURL
827852 // to construct the absolute URL, then return an HTTP-loaded module immediately.
853+ // Security: Gated by IsHttpUrlAllowedForLoading.
828854 bool referrerIsHttp = (!referrerPath.empty () && (StartsWith (referrerPath, " http://" ) || StartsWith (referrerPath, " https://" )));
829855 bool specIsRootAbs = !spec.empty () && spec[0 ] == ' /' ;
830856 if (referrerIsHttp && (specIsRelative || specIsRootAbs)) {
@@ -845,6 +871,16 @@ static bool IsDocumentsPath(const std::string& path) {
845871 }
846872 }
847873 if (!resolvedHttp.empty () && (StartsWith (resolvedHttp, " http://" ) || StartsWith (resolvedHttp, " https://" ))) {
874+ // Security check: block if remote modules not allowed
875+ if (!IsHttpUrlAllowedForLoading (resolvedHttp)) {
876+ std::string msg = GetRemoteModuleBlockedMessage (resolvedHttp);
877+ if (IsScriptLoadingLogEnabled ()) {
878+ Log (@" [resolver][security] blocked remote module (rel): %s " , resolvedHttp.c_str ());
879+ }
880+ isolate->ThrowException (v8::Exception::Error (tns::ToV8String (isolate, msg.c_str ())));
881+ return v8::MaybeLocal<v8::Module>();
882+ }
883+
848884 if (IsScriptLoadingLogEnabled ()) {
849885 Log (@" [resolver][http-rel] base=%s spec=%s -> %s " , referrerPath.c_str (), spec.c_str (), resolvedHttp.c_str ());
850886 }
@@ -1010,6 +1046,16 @@ static bool IsDocumentsPath(const std::string& path) {
10101046
10111047 // If the specifier is an HTTP(S) URL, fetch via HTTP loader and return
10121048 if (StartsWith (spec, " http://" ) || StartsWith (spec, " https://" )) {
1049+ // Security check: block if remote modules not allowed
1050+ if (!IsHttpUrlAllowedForLoading (spec)) {
1051+ std::string msg = GetRemoteModuleBlockedMessage (spec);
1052+ if (IsScriptLoadingLogEnabled ()) {
1053+ Log (@" [resolver][security] blocked remote module: %s " , spec.c_str ());
1054+ }
1055+ isolate->ThrowException (v8::Exception::Error (tns::ToV8String (isolate, msg.c_str ())));
1056+ return v8::MaybeLocal<v8::Module>();
1057+ }
1058+
10131059 std::string key = CanonicalizeHttpUrlKey (spec);
10141060 if (IsScriptLoadingLogEnabled ()) {
10151061 Log (@" [http-esm][compile][begin] %s " , key.c_str ());
@@ -1095,6 +1141,7 @@ static bool IsDocumentsPath(const std::string& path) {
10951141
10961142 // If a candidate accidentally embeds a collapsed HTTP URL like '/app/http:/host/...',
10971143 // reconstruct the HTTP URL and resolve via the HTTP loader instead of touching the filesystem.
1144+ // Security: Gated by IsHttpUrlAllowedForLoading.
10981145 auto rerouteHttpIfEmbedded = [&](const std::string& p) -> bool {
10991146 size_t pos1 = p.find (" /http:/" );
11001147 size_t pos2 = p.find (" /https:/" );
@@ -1108,6 +1155,17 @@ static bool IsDocumentsPath(const std::string& path) {
11081155 tail.insert (6 , " /" );
11091156 }
11101157 if (!(StartsWith (tail, " http://" ) || StartsWith (tail, " https://" ))) return false ;
1158+
1159+ // Security check: block if remote modules not allowed
1160+ if (!IsHttpUrlAllowedForLoading (tail)) {
1161+ if (IsScriptLoadingLogEnabled ()) {
1162+ Log (@" [resolver][security] blocked embedded remote module: %s " , tail.c_str ());
1163+ }
1164+ std::string msg = GetRemoteModuleBlockedMessage (tail);
1165+ isolate->ThrowException (v8::Exception::Error (tns::ToV8String (isolate, msg.c_str ())));
1166+ return false ;
1167+ }
1168+
11111169 if (IsScriptLoadingLogEnabled ()) { Log (@" [resolver][http-embedded] %s -> %s " , p.c_str (), tail.c_str ()); }
11121170 std::string key = CanonicalizeHttpUrlKey (tail);
11131171 auto itExisting = g_moduleRegistry.find (key);
@@ -1913,6 +1971,16 @@ static bool IsDocumentsPath(const std::string& path) {
19131971
19141972 // If spec is an HTTP(S) URL, try HTTP fetch+compile directly
19151973 if (!normalizedSpec.empty () && (StartsWith (normalizedSpec, " http://" ) || StartsWith (normalizedSpec, " https://" ))) {
1974+ // Security check: block if remote modules not allowed
1975+ if (!IsHttpUrlAllowedForLoading (normalizedSpec)) {
1976+ std::string msg = GetRemoteModuleBlockedMessage (normalizedSpec);
1977+ if (IsScriptLoadingLogEnabled ()) {
1978+ Log (@" [dyn-import][security] blocked remote module: %s " , normalizedSpec.c_str ());
1979+ }
1980+ resolver->Reject (context, v8::Exception::Error (tns::ToV8String (isolate, msg.c_str ()))).FromMaybe (false );
1981+ return scope.Escape (resolver->GetPromise ());
1982+ }
1983+
19161984 if (IsScriptLoadingLogEnabled ()) {
19171985 Log (@" [dyn-import][http-loader] trying URL %s " , normalizedSpec.c_str ());
19181986 }
0 commit comments