@@ -2,15 +2,19 @@ import { Test, TestingModule } from '@nestjs/testing';
22import { HttpService } from '@nestjs/axios' ;
33import { of , throwError } from 'rxjs' ;
44import * as blake from 'blakejs' ;
5+ import { lookup } from 'node:dns/promises' ;
56
67import { AppService } from './app.service' ;
78import { ValidateMetadataDTO } from '@dto' ;
89import { MetadataValidationStatus } from '@enums' ;
910import { MetadataStandard } from '@types' ;
10- import { validateMetadataStandard , parseMetadata } from '@utils' ;
11+ import { validateMetadataStandard , parseMetadata , getStandard } from '@utils' ;
1112import { AxiosResponse , AxiosRequestHeaders } from 'axios' ;
1213
1314jest . mock ( '@utils' ) ;
15+ jest . mock ( 'node:dns/promises' , ( ) => ( {
16+ lookup : jest . fn ( ) ,
17+ } ) ) ;
1418
1519describe ( 'AppService' , ( ) => {
1620 let service : AppService ;
@@ -31,19 +35,20 @@ describe('AppService', () => {
3135
3236 service = module . get < AppService > ( AppService ) ;
3337 httpService = module . get < HttpService > ( HttpService ) ;
38+ ( lookup as jest . Mock ) . mockResolvedValue ( [ { address : '93.184.216.34' } ] ) ;
3439 } ) ;
3540
3641 it ( 'should validate metadata correctly' , async ( ) => {
3742 const url = 'http://example.com' ;
3843 const hash = 'correctHash' ;
3944 const validateMetadataDTO : ValidateMetadataDTO = { hash, url } ;
40- const data = {
45+ const body = {
4146 body : 'testBody' ,
4247 headers : { } ,
4348 } ;
4449 const parsedMetadata = { parsed : 'metadata' } ;
4550 const response : AxiosResponse = {
46- data,
51+ data : JSON . stringify ( body ) ,
4752 status : 200 ,
4853 statusText : 'OK' ,
4954 headers : { } ,
@@ -53,6 +58,7 @@ describe('AppService', () => {
5358 } ,
5459 } ;
5560 jest . spyOn ( httpService , 'get' ) . mockReturnValueOnce ( of ( response ) ) ;
61+ ( getStandard as jest . Mock ) . mockReturnValueOnce ( MetadataStandard . CIP108 ) ;
5662 ( validateMetadataStandard as jest . Mock ) . mockResolvedValueOnce ( undefined ) ;
5763 ( parseMetadata as jest . Mock ) . mockReturnValueOnce ( parsedMetadata ) ;
5864 jest . spyOn ( blake , 'blake2bHex' ) . mockReturnValueOnce ( hash ) ;
@@ -65,12 +71,16 @@ describe('AppService', () => {
6571 metadata : parsedMetadata ,
6672 } ) ;
6773 expect ( validateMetadataStandard ) . toHaveBeenCalledWith (
68- data ,
74+ body . body ,
6975 MetadataStandard . CIP108 ,
7076 ) ;
71- expect ( parseMetadata ) . toHaveBeenCalledWith (
72- data . body ,
73- MetadataStandard . CIP108 ,
77+ expect ( parseMetadata ) . toHaveBeenCalledWith ( body . body ) ;
78+ expect ( httpService . get ) . toHaveBeenCalledWith (
79+ url ,
80+ expect . objectContaining ( {
81+ httpAgent : expect . any ( Object ) ,
82+ httpsAgent : expect . any ( Object ) ,
83+ } ) ,
7484 ) ;
7585 } ) ;
7686
@@ -98,13 +108,13 @@ describe('AppService', () => {
98108 const url = 'http://example.com' ;
99109 const hash = 'incorrectHash' ;
100110 const validateMetadataDTO : ValidateMetadataDTO = { hash, url } ;
101- const data = {
111+ const body = {
102112 body : 'testBody' ,
103113 } ;
104114 const parsedMetadata = { parsed : 'metadata' } ;
105115
106116 const response : AxiosResponse = {
107- data,
117+ data : JSON . stringify ( body ) ,
108118 status : 200 ,
109119 statusText : 'OK' ,
110120 headers : { } ,
@@ -114,6 +124,7 @@ describe('AppService', () => {
114124 } ,
115125 } ;
116126 jest . spyOn ( httpService , 'get' ) . mockReturnValueOnce ( of ( response ) ) ;
127+ ( getStandard as jest . Mock ) . mockReturnValueOnce ( MetadataStandard . CIP108 ) ;
117128 ( validateMetadataStandard as jest . Mock ) . mockResolvedValueOnce ( undefined ) ;
118129 ( parseMetadata as jest . Mock ) . mockReturnValueOnce ( parsedMetadata ) ;
119130 jest . spyOn ( blake , 'blake2bHex' ) . mockReturnValueOnce ( 'differentHash' ) ;
@@ -126,4 +137,82 @@ describe('AppService', () => {
126137 metadata : parsedMetadata ,
127138 } ) ;
128139 } ) ;
140+
141+ it ( 'should block loopback metadata URLs before fetching' , async ( ) => {
142+ const validateMetadataDTO : ValidateMetadataDTO = {
143+ hash : 'hash' ,
144+ url : 'http://127.0.0.1:3000/api' ,
145+ } ;
146+
147+ const result = await service . validateMetadata ( validateMetadataDTO ) ;
148+
149+ expect ( result ) . toEqual ( {
150+ status : MetadataValidationStatus . URL_BLOCKED ,
151+ valid : false ,
152+ metadata : undefined ,
153+ } ) ;
154+ expect ( httpService . get ) . not . toHaveBeenCalled ( ) ;
155+ } ) ;
156+
157+ it ( 'should block hostnames that resolve to private addresses' , async ( ) => {
158+ ( lookup as jest . Mock ) . mockResolvedValueOnce ( [ { address : '10.0.0.5' } ] ) ;
159+
160+ const validateMetadataDTO : ValidateMetadataDTO = {
161+ hash : 'hash' ,
162+ url : 'https://metadata.internal.example/metadata.json' ,
163+ } ;
164+
165+ const result = await service . validateMetadata ( validateMetadataDTO ) ;
166+
167+ expect ( result ) . toEqual ( {
168+ status : MetadataValidationStatus . URL_BLOCKED ,
169+ valid : false ,
170+ metadata : undefined ,
171+ } ) ;
172+ expect ( httpService . get ) . not . toHaveBeenCalled ( ) ;
173+ } ) ;
174+
175+ it ( 'should block private addresses during the HTTP agent lookup' , async ( ) => {
176+ const url = 'http://example.com' ;
177+ const hash = 'correctHash' ;
178+ const body = {
179+ body : 'testBody' ,
180+ } ;
181+ const response : AxiosResponse = {
182+ data : JSON . stringify ( body ) ,
183+ status : 200 ,
184+ statusText : 'OK' ,
185+ headers : { } ,
186+ config : {
187+ headers : { } as AxiosRequestHeaders ,
188+ url,
189+ } ,
190+ } ;
191+ jest . spyOn ( httpService , 'get' ) . mockReturnValueOnce ( of ( response ) ) ;
192+ ( getStandard as jest . Mock ) . mockReturnValueOnce ( MetadataStandard . CIP108 ) ;
193+ ( validateMetadataStandard as jest . Mock ) . mockResolvedValueOnce ( undefined ) ;
194+ jest . spyOn ( blake , 'blake2bHex' ) . mockReturnValueOnce ( hash ) ;
195+
196+ await service . validateMetadata ( { hash, url } ) ;
197+
198+ const requestConfig = ( httpService . get as jest . Mock ) . mock . calls [ 0 ] [ 1 ] ;
199+ const agentLookup = requestConfig . httpAgent . options . lookup ;
200+ ( lookup as jest . Mock ) . mockResolvedValueOnce ( {
201+ address : '127.0.0.1' ,
202+ family : 4 ,
203+ } ) ;
204+
205+ await expect (
206+ new Promise ( ( resolve , reject ) => {
207+ agentLookup ( 'example.com' , { } , ( error : Error | null ) => {
208+ if ( error ) {
209+ reject ( error ) ;
210+ return ;
211+ }
212+
213+ resolve ( undefined ) ;
214+ } ) ;
215+ } ) ,
216+ ) . rejects . toThrow ( MetadataValidationStatus . URL_BLOCKED ) ;
217+ } ) ;
129218} ) ;
0 commit comments