@@ -92,6 +92,11 @@ pub fn parse_replacement(input: &[&str]) -> jsg::Result<String, DnsParserError>
9292 // Each frame starts with the length of the remaining frame.
9393 while length_index < input. len ( ) {
9494 let length = usize:: from_str_radix ( input[ length_index] , 16 ) ?;
95+ if length + offset_index > input. len ( ) {
96+ return Err ( DnsParserError :: InvalidDnsResponse (
97+ "replacement data too short for declared frame length" . to_owned ( ) ,
98+ ) ) ;
99+ }
95100 let subset = input[ offset_index..length + offset_index] . to_vec ( ) ;
96101 let decoded = decode_hex ( & subset) ?. join ( "" ) ;
97102
@@ -144,10 +149,26 @@ impl DnsUtil {
144149 #[ jsg_method]
145150 pub fn parse_caa_record ( & self , record : String ) -> Result < CaaRecord , DnsParserError > {
146151 // Let's remove "\\#" and the length of data from the beginning of the record
147- let data = record. split_ascii_whitespace ( ) . collect :: < Vec < _ > > ( ) [ 2 ..] . to_vec ( ) ;
152+ let parts: Vec < _ > = record. split_ascii_whitespace ( ) . collect ( ) ;
153+ if parts. len ( ) < 3 {
154+ return Err ( DnsParserError :: InvalidDnsResponse (
155+ "CAA record too short: expected at least 3 fields" . to_owned ( ) ,
156+ ) ) ;
157+ }
158+ let data = parts[ 2 ..] . to_vec ( ) ;
159+ if data. len ( ) < 2 {
160+ return Err ( DnsParserError :: InvalidDnsResponse (
161+ "CAA record data too short: expected critical and prefix length fields" . to_owned ( ) ,
162+ ) ) ;
163+ }
148164 let critical = data[ 0 ] . parse :: < u8 > ( ) ?;
149165 let prefix_length = data[ 1 ] . parse :: < usize > ( ) ?;
150166
167+ if data. len ( ) < 2 + prefix_length {
168+ return Err ( DnsParserError :: InvalidDnsResponse ( format ! (
169+ "CAA record data too short for prefix_length {prefix_length}"
170+ ) ) ) ;
171+ }
151172 let field = decode_hex ( & data[ 2 ..prefix_length + 2 ] ) ?. join ( "" ) ;
152173 let value = decode_hex ( & data[ ( prefix_length + 2 ) ..] ) ?. join ( "" ) ;
153174
@@ -198,7 +219,20 @@ impl DnsUtil {
198219 /// `DnsParserError::ParseIntError`
199220 #[ jsg_method]
200221 pub fn parse_naptr_record ( & self , record : String ) -> jsg:: Result < NaptrRecord , DnsParserError > {
201- let data = record. split_ascii_whitespace ( ) . collect :: < Vec < _ > > ( ) [ 1 ..] . to_vec ( ) ;
222+ let parts: Vec < _ > = record. split_ascii_whitespace ( ) . collect ( ) ;
223+ if parts. len ( ) < 2 {
224+ return Err ( DnsParserError :: InvalidDnsResponse (
225+ "NAPTR record too short" . to_owned ( ) ,
226+ ) ) ;
227+ }
228+ let data = parts[ 1 ..] . to_vec ( ) ;
229+
230+ // Need at least: length(1) + order(2) + preference(2) + flag_length(1) = 6 fields
231+ if data. len ( ) < 6 {
232+ return Err ( DnsParserError :: InvalidDnsResponse (
233+ "NAPTR record data too short: expected at least 6 fields" . to_owned ( ) ,
234+ ) ) ;
235+ }
202236
203237 let order_str = data[ 1 ..3 ] . to_vec ( ) ;
204238 let order = u32:: from_str_radix ( & order_str. join ( "" ) , 16 ) ?;
@@ -207,14 +241,29 @@ impl DnsUtil {
207241
208242 let flag_length = usize:: from_str_radix ( data[ 5 ] , 16 ) ?;
209243 let flag_offset = 6 ;
244+ if data. len ( ) < flag_offset + flag_length + 1 {
245+ return Err ( DnsParserError :: InvalidDnsResponse (
246+ "NAPTR record too short for flags field" . to_owned ( ) ,
247+ ) ) ;
248+ }
210249 let flags = decode_hex ( & data[ flag_offset..flag_length + flag_offset] ) ?. join ( "" ) ;
211250
212251 let service_length = usize:: from_str_radix ( data[ flag_offset + flag_length] , 16 ) ?;
213252 let service_offset = flag_offset + flag_length + 1 ;
253+ if data. len ( ) < service_offset + service_length + 1 {
254+ return Err ( DnsParserError :: InvalidDnsResponse (
255+ "NAPTR record too short for service field" . to_owned ( ) ,
256+ ) ) ;
257+ }
214258 let service = decode_hex ( & data[ service_offset..service_length + service_offset] ) ?. join ( "" ) ;
215259
216260 let regexp_length = usize:: from_str_radix ( data[ service_offset + service_length] , 16 ) ?;
217261 let regexp_offset = service_offset + service_length + 1 ;
262+ if data. len ( ) < regexp_offset + regexp_length {
263+ return Err ( DnsParserError :: InvalidDnsResponse (
264+ "NAPTR record too short for regexp field" . to_owned ( ) ,
265+ ) ) ;
266+ }
218267 let regexp = decode_hex ( & data[ regexp_offset..regexp_length + regexp_offset] ) ?. join ( "" ) ;
219268
220269 let replacement = parse_replacement ( & data[ regexp_offset + regexp_length..] ) ?;
@@ -321,4 +370,94 @@ mod tests {
321370 assert_eq ! ( record. order, 5555 ) ;
322371 assert_eq ! ( record. preference, 2222 ) ;
323372 }
373+
374+ // =========================================================================
375+ // Malformed input tests — these previously caused panics (index out of bounds)
376+ // which would abort the process via CXX. They must return Err, not panic.
377+ // =========================================================================
378+
379+ #[ test]
380+ fn test_parse_caa_record_empty_string ( ) {
381+ let dns_util = DnsUtil {
382+ _state : ResourceState :: default ( ) ,
383+ } ;
384+ assert ! ( dns_util. parse_caa_record( String :: new( ) ) . is_err( ) ) ;
385+ }
386+
387+ #[ test]
388+ fn test_parse_caa_record_single_token ( ) {
389+ let dns_util = DnsUtil {
390+ _state : ResourceState :: default ( ) ,
391+ } ;
392+ assert ! ( dns_util. parse_caa_record( "\\ #" . to_owned( ) ) . is_err( ) ) ;
393+ }
394+
395+ #[ test]
396+ fn test_parse_caa_record_two_tokens ( ) {
397+ let dns_util = DnsUtil {
398+ _state : ResourceState :: default ( ) ,
399+ } ;
400+ assert ! ( dns_util. parse_caa_record( "\\ # 15" . to_owned( ) ) . is_err( ) ) ;
401+ }
402+
403+ #[ test]
404+ fn test_parse_caa_record_data_too_short_for_prefix ( ) {
405+ let dns_util = DnsUtil {
406+ _state : ResourceState :: default ( ) ,
407+ } ;
408+ // critical=00, prefix_length=FF (255) but no data follows
409+ assert ! (
410+ dns_util
411+ . parse_caa_record( "\\ # 02 00 FF" . to_owned( ) )
412+ . is_err( )
413+ ) ;
414+ }
415+
416+ #[ test]
417+ fn test_parse_naptr_record_empty_string ( ) {
418+ let dns_util = DnsUtil {
419+ _state : ResourceState :: default ( ) ,
420+ } ;
421+ assert ! ( dns_util. parse_naptr_record( String :: new( ) ) . is_err( ) ) ;
422+ }
423+
424+ #[ test]
425+ fn test_parse_naptr_record_single_token ( ) {
426+ let dns_util = DnsUtil {
427+ _state : ResourceState :: default ( ) ,
428+ } ;
429+ assert ! ( dns_util. parse_naptr_record( "\\ #" . to_owned( ) ) . is_err( ) ) ;
430+ }
431+
432+ #[ test]
433+ fn test_parse_naptr_record_too_few_fields ( ) {
434+ let dns_util = DnsUtil {
435+ _state : ResourceState :: default ( ) ,
436+ } ;
437+ assert ! (
438+ dns_util
439+ . parse_naptr_record( "\\ # 37 15 b3" . to_owned( ) )
440+ . is_err( )
441+ ) ;
442+ }
443+
444+ #[ test]
445+ fn test_parse_replacement_length_exceeds_input ( ) {
446+ // First element says frame is FF (255) bytes but only 2 bytes follow
447+ let input = vec ! [ "FF" , "73" , "69" ] ;
448+ assert ! ( parse_replacement( & input) . is_err( ) ) ;
449+ }
450+
451+ #[ test]
452+ fn test_parse_naptr_record_truncated_at_flags ( ) {
453+ let dns_util = DnsUtil {
454+ _state : ResourceState :: default ( ) ,
455+ } ;
456+ // Has order+preference+flag_length but no flag data
457+ assert ! (
458+ dns_util
459+ . parse_naptr_record( "\\ # 06 15 b3 08 ae 05" . to_owned( ) )
460+ . is_err( )
461+ ) ;
462+ }
324463}
0 commit comments