Skip to content

Commit 8ac8845

Browse files
authored
Merge pull request #343 from digitalmoksha/bw-sync-with-0.29.0.gfm.12
Sync with cmark-gfm-0.29.0.gfm.12
2 parents 8f3e665 + e5bab5a commit 8ac8845

11 files changed

+166
-33
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Collective](https://opencollective.com/comrak/all/badge.svg?label=financial+cont
66
[![crates.io version](https://img.shields.io/crates/v/comrak.svg)](https://crates.io/crates/comrak)
77
[![docs.rs](https://docs.rs/comrak/badge.svg)](https://docs.rs/comrak)
88

9-
Rust port of [github's `cmark-gfm`](https://github.com/github/cmark). *Currently synced with release `0.29.0.gfm.11`*.
9+
Rust port of [github's `cmark-gfm`](https://github.com/github/cmark). *Currently synced with release `0.29.0.gfm.12`*.
1010

1111
- [Installation](#installation)
1212
- [Usage](#usage)

src/cm.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::ctype::{isalpha, isdigit, ispunct, isspace};
22
use crate::nodes::TableAlignment;
33
use crate::nodes::{
44
AstNode, ListDelimType, ListType, NodeCodeBlock, NodeHeading, NodeHtmlBlock, NodeLink,
5-
NodeValue,
5+
NodeTable, NodeValue,
66
};
77
#[cfg(feature = "shortcodes")]
88
use crate::parser::shortcodes::NodeShortCode;
@@ -732,7 +732,7 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> {
732732
if in_header && node.next_sibling().is_none() {
733733
let table = &node.parent().unwrap().parent().unwrap().data.borrow().value;
734734
let alignments = match *table {
735-
NodeValue::Table(ref alignments) => alignments,
735+
NodeValue::Table(NodeTable { ref alignments, .. }) => alignments,
736736
_ => panic!(),
737737
};
738738

src/html.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! The HTML renderer for the CommonMark AST, as well as helper functions.
22
use crate::ctype::isspace;
33
use crate::nodes::{
4-
AstNode, ListType, NodeCode, NodeFootnoteDefinition, NodeValue, TableAlignment,
4+
AstNode, ListType, NodeCode, NodeFootnoteDefinition, NodeTable, NodeValue, TableAlignment,
55
};
66
use crate::parser::{Options, Plugins};
77
use crate::scanners;
@@ -893,7 +893,7 @@ impl<'o> HtmlFormatter<'o> {
893893

894894
let table = &node.parent().unwrap().parent().unwrap().data.borrow().value;
895895
let alignments = match *table {
896-
NodeValue::Table(ref alignments) => alignments,
896+
NodeValue::Table(NodeTable { ref alignments, .. }) => alignments,
897897
_ => panic!(),
898898
};
899899

src/nodes.rs

+17-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ pub enum NodeValue {
9292

9393
/// **Block**. A [table](https://github.github.com/gfm/#tables-extension-) per the GFM spec.
9494
/// Contains table rows.
95-
Table(Vec<TableAlignment>),
95+
Table(NodeTable),
9696

9797
/// **Block**. A table row. The `bool` represents whether the row is the header row or not.
9898
/// Contains table cells.
@@ -180,6 +180,22 @@ impl TableAlignment {
180180
}
181181
}
182182

183+
/// The metadata of a table
184+
#[derive(Debug, Default, Clone, PartialEq, Eq)]
185+
pub struct NodeTable {
186+
/// The table alignments
187+
pub alignments: Vec<TableAlignment>,
188+
189+
/// Number of columns of the table
190+
pub num_columns: usize,
191+
192+
/// Number of rows of the table
193+
pub num_rows: usize,
194+
195+
/// Number of non-empty, non-autocompleted cells
196+
pub num_nonempty_cells: usize,
197+
}
198+
183199
/// An inline [code span](https://github.github.com/gfm/#code-spans).
184200
#[derive(Debug, Clone, PartialEq, Eq)]
185201
pub struct NodeCode {

src/parser/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1105,6 +1105,7 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> {
11051105
self.advance_offset(line, adv, false);
11061106
} else if !indented
11071107
&& self.options.extension.footnotes
1108+
&& depth < MAX_LIST_DEPTH
11081109
&& unwrap_into(
11091110
scanners::footnote_definition(&line[self.first_nonspace..]),
11101111
&mut matched,

src/parser/table.rs

+64-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::arena_tree::Node;
22
use crate::nodes;
3-
use crate::nodes::{Ast, AstNode, NodeValue, TableAlignment};
3+
use crate::nodes::{Ast, AstNode, NodeTable, NodeValue, TableAlignment};
44
use crate::parser::Parser;
55
use crate::scanners;
66
use crate::strings::trim;
@@ -9,14 +9,17 @@ use std::cmp::min;
99

1010
use super::inlines::count_newlines;
1111

12+
// Limit to prevent a malicious input from causing a denial of service.
13+
const MAX_AUTOCOMPLETED_CELLS: usize = 500_000;
14+
1215
pub fn try_opening_block<'a>(
1316
parser: &mut Parser<'a, '_, '_>,
1417
container: &'a AstNode<'a>,
1518
line: &[u8],
1619
) -> Option<(&'a AstNode<'a>, bool, bool)> {
1720
let aligns = match container.data.borrow().value {
1821
NodeValue::Paragraph => None,
19-
NodeValue::Table(ref aligns) => Some(aligns.clone()),
22+
NodeValue::Table(NodeTable { ref alignments, .. }) => Some(alignments.clone()),
2023
_ => return None,
2124
};
2225

@@ -74,7 +77,15 @@ fn try_opening_header<'a>(
7477
}
7578

7679
let start = container.data.borrow().sourcepos.start;
77-
let child = Ast::new(NodeValue::Table(alignments), start);
80+
let child = Ast::new(
81+
NodeValue::Table(NodeTable {
82+
alignments,
83+
num_columns: header_row.cells.len(),
84+
num_rows: 0,
85+
num_nonempty_cells: 0,
86+
}),
87+
start,
88+
);
7889
let table = parser.arena.alloc(Node::new(RefCell::new(child)));
7990
container.append(table);
8091

@@ -88,7 +99,10 @@ fn try_opening_header<'a>(
8899
);
89100
}
90101

91-
for cell in header_row.cells {
102+
let mut i = 0;
103+
104+
while i < header_row.cells.len() {
105+
let cell = &header_row.cells[i];
92106
let ast_cell = parser.add_child(
93107
header,
94108
NodeValue::TableCell,
@@ -100,8 +114,12 @@ fn try_opening_header<'a>(
100114
start.column_add((cell.end_offset - header_row.paragraph_offset) as isize);
101115
ast.internal_offset = cell.internal_offset;
102116
ast.content = cell.content.clone();
117+
118+
i += 1;
103119
}
104120

121+
incr_table_row_count(container, i);
122+
105123
let offset = line.len() - 1 - parser.offset;
106124
parser.advance_offset(line, offset, false);
107125

@@ -117,6 +135,11 @@ fn try_opening_row<'a>(
117135
if parser.blank {
118136
return None;
119137
}
138+
139+
if get_num_autocompleted_cells(container) > MAX_AUTOCOMPLETED_CELLS {
140+
return None;
141+
}
142+
120143
let sourcepos = container.data.borrow().sourcepos;
121144
let this_row = match row(&line[parser.first_nonspace..]) {
122145
Some(this_row) => this_row,
@@ -148,9 +171,12 @@ fn try_opening_row<'a>(
148171
cell_ast.content = cell.content.clone();
149172

150173
last_column = cell_ast.sourcepos.end.column;
174+
151175
i += 1;
152176
}
153177

178+
incr_table_row_count(container, i);
179+
154180
while i < alignments.len() {
155181
parser.add_child(new_row, NodeValue::TableCell, last_column);
156182
i += 1;
@@ -305,6 +331,40 @@ fn unescape_pipes(string: &[u8]) -> Vec<u8> {
305331
v
306332
}
307333

334+
// Increment the number of rows in the table. Also update n_nonempty_cells,
335+
// which keeps track of the number of cells which were parsed from the
336+
// input file. (If one of the rows is too short, then the trailing cells
337+
// are autocompleted. Autocompleted cells are not counted in n_nonempty_cells.)
338+
// The purpose of this is to prevent a malicious input from generating a very
339+
// large number of autocompleted cells, which could cause a denial of service
340+
// vulnerability.
341+
fn incr_table_row_count<'a>(container: &'a AstNode<'a>, i: usize) -> bool {
342+
return match container.data.borrow_mut().value {
343+
NodeValue::Table(ref mut node_table) => {
344+
node_table.num_rows += 1;
345+
node_table.num_nonempty_cells += i;
346+
true
347+
}
348+
_ => false,
349+
};
350+
}
351+
352+
// Calculate the number of autocompleted cells.
353+
fn get_num_autocompleted_cells<'a>(container: &'a AstNode<'a>) -> usize {
354+
return match container.data.borrow().value {
355+
NodeValue::Table(ref node_table) => {
356+
let num_cells = node_table.num_columns * node_table.num_rows;
357+
358+
if num_cells < node_table.num_nonempty_cells {
359+
0
360+
} else {
361+
(node_table.num_columns * node_table.num_rows) - node_table.num_nonempty_cells
362+
}
363+
}
364+
_ => 0,
365+
};
366+
}
367+
308368
pub fn matches(line: &[u8]) -> bool {
309369
row(line).is_some()
310370
}

src/tests.rs

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod footnotes;
1313
mod fuzz;
1414
mod header_ids;
1515
mod options;
16+
mod pathological;
1617
mod plugins;
1718
mod propfuzz;
1819
mod regressions;

src/tests/api.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,11 @@ fn exercise_full_api() {
166166
let _: &String = &nfd.name;
167167
let _: u32 = nfd.total_references;
168168
}
169-
nodes::NodeValue::Table(aligns) => {
170-
let _: &Vec<nodes::TableAlignment> = aligns;
171-
match aligns[0] {
169+
nodes::NodeValue::Table(nt) => {
170+
let _: &Vec<nodes::TableAlignment> = &nt.alignments;
171+
let _: usize = nt.num_nonempty_cells;
172+
let _: usize = nt.num_rows;
173+
match nt.alignments[0] {
172174
nodes::TableAlignment::None => {}
173175
nodes::TableAlignment::Left => {}
174176
nodes::TableAlignment::Center => {}

src/tests/pathological.rs

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use super::*;
2+
use ntest::timeout;
3+
4+
// input: python3 -c 'n = 50000; print("*a_ " * n)'
5+
#[test]
6+
#[timeout(4000)]
7+
fn pathological_emphases() {
8+
let n = 50_000;
9+
let input = format!("{}", "*a_ ".repeat(n));
10+
let mut exp = format!("<p>{}", input);
11+
// Right-most space is trimmed in output.
12+
exp.pop();
13+
exp += "</p>\n";
14+
15+
html(&input, &exp);
16+
}
17+
18+
// input: python3 -c 'n = 10000; print("|" + "x|" * n + "\n|" + "-|" * n)'
19+
#[test]
20+
#[timeout(4000)]
21+
fn pathological_table_columns_1() {
22+
let n = 100_000;
23+
let input = format!("{}{}{}{}", "|", "x|".repeat(n), "\n|", "-|".repeat(n));
24+
let exp = format!("<p>{}</p>\n", input);
25+
26+
html_opts!([extension.table], &input, &exp);
27+
}
28+
29+
// input: python3 -c 'n = 70000; print("|" + "x|" * n + "\n|" + "-|" * n + "\n" + "a\n" * n)'
30+
#[test]
31+
#[timeout(4000)]
32+
fn pathological_table_columns_2() {
33+
let n = 100_000;
34+
let input = format!(
35+
"{}{}{}{}{}{}",
36+
"|",
37+
"x|".repeat(n),
38+
"\n|",
39+
"-|".repeat(n),
40+
"\n",
41+
"a\n".repeat(n)
42+
);
43+
44+
let mut extension = ExtensionOptions::default();
45+
extension.table = true;
46+
47+
// Not interested in the actual html, just that we don't timeout
48+
markdown_to_html(
49+
&input,
50+
&Options {
51+
extension,
52+
parse: Default::default(),
53+
render: RenderOptions::default(),
54+
},
55+
);
56+
}
57+
58+
// input: python3 -c 'n = 10000; print("[^1]:" * n + "\n" * n)'
59+
#[test]
60+
#[timeout(4000)]
61+
fn pathological_footnotes() {
62+
let n = 10_000;
63+
let input = format!("{}{}", "[^1]:".repeat(n), "\n".repeat(n));
64+
let exp = "";
65+
66+
html_opts!([extension.footnotes], &input, &exp);
67+
}

src/tests/regressions.rs

-16
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,6 @@ fn regression_back_to_back_ranges() {
5656
);
5757
}
5858

59-
#[test]
60-
#[timeout(4000)]
61-
fn pathological_emphases() {
62-
let mut s = String::with_capacity(50000 * 4);
63-
for _ in 0..50000 {
64-
s.push_str("*a_ ");
65-
}
66-
67-
let mut exp = format!("<p>{}", s);
68-
// Right-most space is trimmed in output.
69-
exp.pop();
70-
exp += "</p>\n";
71-
72-
html(&s, &exp);
73-
}
74-
7559
#[test]
7660
fn no_panic_on_empty_bookended_atx_headers() {
7761
html("# #", "<h1></h1>\n");

src/xml.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::nodes::{AstNode, ListType, NodeCode, NodeValue};
1+
use crate::nodes::{AstNode, ListType, NodeCode, NodeTable, NodeValue};
22
use crate::parser::{Options, Plugins};
33
use once_cell::sync::Lazy;
44
use std::cmp;
@@ -218,11 +218,13 @@ impl<'o> XmlFormatter<'o> {
218218
let header_row = &ancestors.next().unwrap().data.borrow().value;
219219
let table = &ancestors.next().unwrap().data.borrow().value;
220220

221-
if let (NodeValue::TableRow(true), NodeValue::Table(aligns)) =
222-
(header_row, table)
221+
if let (
222+
NodeValue::TableRow(true),
223+
NodeValue::Table(NodeTable { alignments, .. }),
224+
) = (header_row, table)
223225
{
224226
let ix = node.preceding_siblings().count() - 1;
225-
if let Some(xml_align) = aligns[ix].xml_name() {
227+
if let Some(xml_align) = alignments[ix].xml_name() {
226228
write!(self.output, " align=\"{}\"", xml_align)?;
227229
}
228230
}

0 commit comments

Comments
 (0)