@blamejs/core 0.18.43 → 0.18.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -0
- package/NOTICE +1 -1
- package/README.md +3 -2
- package/index.js +2 -0
- package/lib/chain-writer.js +103 -4
- package/lib/file-type.js +19 -2
- package/lib/framework-error.js +10 -0
- package/lib/gate-contract.js +255 -12
- package/lib/guard-all.js +1 -0
- package/lib/guard-archive.js +14 -0
- package/lib/guard-auth.js +1 -3
- package/lib/guard-country.js +480 -0
- package/lib/guard-csv.js +5 -0
- package/lib/guard-email.js +7 -7
- package/lib/guard-filename.js +11 -9
- package/lib/guard-graphql.js +1 -3
- package/lib/guard-html.js +6 -7
- package/lib/guard-image.js +15 -1
- package/lib/guard-json.js +3 -7
- package/lib/guard-jwt.js +8 -3
- package/lib/guard-markdown.js +946 -2
- package/lib/guard-oauth.js +1 -3
- package/lib/guard-pdf.js +8 -0
- package/lib/guard-regex.js +5 -4
- package/lib/guard-smtp-command.js +4 -2
- package/lib/guard-sql.js +10 -0
- package/lib/guard-svg.js +2 -1
- package/lib/guard-text.js +10 -0
- package/lib/guard-yaml.js +13 -2
- package/lib/mail-auth.js +12 -14
- package/lib/middleware/index.js +3 -0
- package/lib/middleware/shared-secret-header.js +334 -0
- package/lib/public-suffix.js +57 -0
- package/lib/time.js +282 -0
- package/lib/vendor/MANIFEST.json +13 -13
- package/lib/vendor/blamejs-pki.cjs +4069 -3138
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/guard-markdown.js
CHANGED
|
@@ -44,6 +44,7 @@ var codepointClass = require("./codepoint-class");
|
|
|
44
44
|
var markupTokenizer = require("./markup-tokenizer");
|
|
45
45
|
var lazyRequire = require("./lazy-require");
|
|
46
46
|
var gateContract = require("./gate-contract");
|
|
47
|
+
var markupEscape = require("./markup-escape").markupEscape;
|
|
47
48
|
var C = require("./constants");
|
|
48
49
|
var { GuardMarkdownError } = require("./framework-error");
|
|
49
50
|
|
|
@@ -262,11 +263,18 @@ function _autolinks(input) {
|
|
|
262
263
|
SCHEME_TAIL_CHARS.indexOf(input.charAt(j)) !== -1) { j += 1; tail += 1; }
|
|
263
264
|
if (input.charAt(j) !== ":") continue;
|
|
264
265
|
var b = j + 1;
|
|
265
|
-
|
|
266
|
+
// The body stops at "<" as well as at ">" and whitespace. An autolink body
|
|
267
|
+
// cannot contain "<" in CommonMark, and running past one let an outer
|
|
268
|
+
// candidate swallow everything nested inside it: `<a:<javascript:alert(1)>>`
|
|
269
|
+
// recorded a single URL beginning `a:`, a scheme nothing objects to, and
|
|
270
|
+
// resumed past the inner candidate so the scheme filter never saw it.
|
|
271
|
+
while (b < input.length && input.charAt(b) !== ">" && input.charAt(b) !== "<" &&
|
|
266
272
|
!markupTokenizer.isMarkupSpace(input.charCodeAt(b))) b += 1;
|
|
267
273
|
// Resume past the scanned body either way. On failure every `<` inside it
|
|
268
274
|
// would rescan the same span, which is quadratic on a document built of
|
|
269
|
-
// `<a:` prefixes.
|
|
275
|
+
// `<a:` prefixes. Stopping at "<" keeps that: the scan still advances to
|
|
276
|
+
// the character that ended it, and that character begins the next
|
|
277
|
+
// candidate rather than being skipped over.
|
|
270
278
|
if (b === j + 1 || input.charAt(b) !== ">") { i = b - 1; continue; }
|
|
271
279
|
out.push({ url: input.slice(i + 1, b), index: i });
|
|
272
280
|
i = b;
|
|
@@ -964,6 +972,936 @@ var INTEGRATION_FIXTURES = Object.freeze({
|
|
|
964
972
|
// buildProfile / compliancePosture / loadRulePack wiring, plus the
|
|
965
973
|
// per-guard inspection surface (validate / sanitize). The bespoke `gate`
|
|
966
974
|
// carries markdown's sanitize-and-reemit chain unchanged.
|
|
975
|
+
// ---- Renderer -------------------------------------------------------------
|
|
976
|
+
//
|
|
977
|
+
// Escape-by-default Markdown to HTML over a deliberately small subset. The
|
|
978
|
+
// three things a hand-rolled emitter gets wrong are the three this closes:
|
|
979
|
+
// author text reaching the output unescaped, a `javascript:` or `data:` URL
|
|
980
|
+
// surviving into an href, and raw HTML passed through in the hope of
|
|
981
|
+
// sanitising it downstream. Each is a stored-XSS hole wherever operator- or
|
|
982
|
+
// author-authored prose is shown to a visitor.
|
|
983
|
+
//
|
|
984
|
+
// The subset is paragraphs, ATX headings, bullet and ordered lists, fenced and
|
|
985
|
+
// indented code, blockquotes, thematic breaks, emphasis, strong, code spans
|
|
986
|
+
// and links. Everything outside it - images, tables, raw HTML, reference
|
|
987
|
+
// links, footnotes - degrades to escaped text. Degrading is the point: an
|
|
988
|
+
// unrecognised construct that renders as its own source is a display bug, and
|
|
989
|
+
// one that renders as markup is a vulnerability.
|
|
990
|
+
//
|
|
991
|
+
// No regex, per the guard-family rule: every scan below is an index walk.
|
|
992
|
+
|
|
993
|
+
// Every text node leaves through here. `apos: "'"` matches guard-html so
|
|
994
|
+
// the two cannot drift on which five characters are escaped.
|
|
995
|
+
// How much larger the rendered output may be than the source it came from.
|
|
996
|
+
//
|
|
997
|
+
// Four, from measurement rather than instinct. Real documents are nowhere near
|
|
998
|
+
// it — this project's README, SECURITY.md and CHANGELOG.md render at 1.22, 1.13
|
|
999
|
+
// and 1.13 — but SHORT constructs carry a fixed markup cost that a small source
|
|
1000
|
+
// cannot absorb: a page of brief fenced samples reaches 3.53, because
|
|
1001
|
+
// `<pre><code class="language-js">` is thirty characters against a fifteen-
|
|
1002
|
+
// character source, and a page of one-line paragraphs reaches 3.00. Three would
|
|
1003
|
+
// have refused a documentation page of code examples, which is worse than the
|
|
1004
|
+
// difference between a 192 MiB and a 256 MiB ceiling on a 64 MiB input.
|
|
1005
|
+
//
|
|
1006
|
+
// The shapes this refuses are further out: a document of nothing but
|
|
1007
|
+
// apostrophes renders at 5.0, and one of nothing but compact links at 7.6.
|
|
1008
|
+
var MAX_OUTPUT_AMPLIFICATION = 4;
|
|
1009
|
+
|
|
1010
|
+
// A SOURCE smaller than this is not bounded by ratio at all.
|
|
1011
|
+
//
|
|
1012
|
+
// A ratio is meaningless on a short document: `hi` renders to `<p>hi</p>\n`,
|
|
1013
|
+
// four times its source, because the fixed cost of a paragraph tag dwarfs two
|
|
1014
|
+
// characters of text. Every small document would be refused. What the ratio
|
|
1015
|
+
// exists to bound is a LARGE input becoming a much larger output, and a small
|
|
1016
|
+
// document's output is small whatever it multiplies by — 20 KiB of apostrophes
|
|
1017
|
+
// renders to 100 KiB, which is nothing to defend against.
|
|
1018
|
+
//
|
|
1019
|
+
// Measured against the SOURCE, not the output allowance. Deriving it from the
|
|
1020
|
+
// allowance instead made the ratio start biting at a quarter of this size,
|
|
1021
|
+
// which is not what the bound says it does.
|
|
1022
|
+
var MIN_SOURCE_FOR_RATIO = C.BYTES.kib(64);
|
|
1023
|
+
|
|
1024
|
+
// Set for the duration of one render() call. Rendering is synchronous from
|
|
1025
|
+
// entry to return, so there is no interleaving to account for; render() clears
|
|
1026
|
+
// it in a finally, including when a cap refuses partway through.
|
|
1027
|
+
var _outputBudget = null;
|
|
1028
|
+
|
|
1029
|
+
// Charge the budget for markup the RENDERER generates, as opposed to author
|
|
1030
|
+
// text, which _escapeText charges. Both count, because both are output: a
|
|
1031
|
+
// six-character `[a](x)` emits a fifty-character anchor with its rel list, so
|
|
1032
|
+
// a document of nothing but compact links amplified 7.6x while the escaped
|
|
1033
|
+
// label and URL inside it stayed tiny. Metering only the author's characters
|
|
1034
|
+
// measured the half that was not growing.
|
|
1035
|
+
function _charge(n) {
|
|
1036
|
+
if (_outputBudget === null) return;
|
|
1037
|
+
_outputBudget.used += n;
|
|
1038
|
+
if (_outputBudget.used > _outputBudget.max) {
|
|
1039
|
+
throw _err("markdown/output-amplification",
|
|
1040
|
+
"b.guardMarkdown.render: rendered output would exceed " +
|
|
1041
|
+
MAX_OUTPUT_AMPLIFICATION + "x the " + _outputBudget.sourceBytes +
|
|
1042
|
+
"-byte source. Both escaping and generated markup expand a document - " +
|
|
1043
|
+
"`'` becomes `'`, and a link becomes an anchor carrying its rel " +
|
|
1044
|
+
"list - so a source written of little else can render to several times " +
|
|
1045
|
+
"its own size.");
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// How long `s` will be once escaped, counted without building it.
|
|
1050
|
+
//
|
|
1051
|
+
// markupEscape expands exactly five characters, and each by a known amount:
|
|
1052
|
+
// `&` -> `&` (+4), `<` -> `<` (+3), `>` -> `>` (+3), `"` -> `"`
|
|
1053
|
+
// (+5) and `'` -> `'` (+4). Counting them is O(n) time and O(1) memory,
|
|
1054
|
+
// which is the point: the budget has to be decided BEFORE the escaped string
|
|
1055
|
+
// exists, or the allocation it exists to prevent has already happened.
|
|
1056
|
+
function _escapedLength(s) {
|
|
1057
|
+
var extra = 0;
|
|
1058
|
+
for (var i = 0; i < s.length; i += 1) {
|
|
1059
|
+
var c = s.charAt(i);
|
|
1060
|
+
if (c === "&") extra += 4;
|
|
1061
|
+
else if (c === "<" || c === ">") extra += 3;
|
|
1062
|
+
else if (c === '"') extra += 5;
|
|
1063
|
+
else if (c === "'") extra += 4;
|
|
1064
|
+
}
|
|
1065
|
+
// BYTES, via Buffer.byteLength, because the allowance is in bytes. Counting
|
|
1066
|
+
// `s.length` would count UTF-16 code units against a UTF-8 budget, and the
|
|
1067
|
+
// two diverge on exactly the input most likely to be adversarial: `é` is one
|
|
1068
|
+
// unit and two bytes, an emoji is two units and four bytes. The expansions
|
|
1069
|
+
// added above are all ASCII, so they are the same number either way.
|
|
1070
|
+
return Buffer.byteLength(s, "utf8") + extra;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function _escapeText(s) {
|
|
1074
|
+
if (_outputBudget !== null) {
|
|
1075
|
+
// Metered HERE because every character of author text passes through this
|
|
1076
|
+
// one function, and PREDICTED rather than measured, because a single span
|
|
1077
|
+
// can be the whole document: one 64 MiB line of apostrophes would build a
|
|
1078
|
+
// 320 MiB string before any check on the result could run.
|
|
1079
|
+
_charge(_escapedLength(s));
|
|
1080
|
+
}
|
|
1081
|
+
return markupEscape(s, { apos: "'" });
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Characters that must never appear inside a quoted attribute value, whatever
|
|
1085
|
+
// the URL parser thinks of them: they end the attribute or open a tag.
|
|
1086
|
+
var _ATTR_UNSAFE = "\"'<>`";
|
|
1087
|
+
|
|
1088
|
+
// A link target is emitted only when it survives BOTH checks: the shared
|
|
1089
|
+
// dangerous-scheme detector (which folds entity and whitespace obfuscation
|
|
1090
|
+
// before testing the scheme) and a literal screen for attribute-breaking
|
|
1091
|
+
// characters. Returns null when the target must not become an href.
|
|
1092
|
+
function _safeHref(url, opts) {
|
|
1093
|
+
if (typeof url !== "string") return null;
|
|
1094
|
+
// Control bytes are screened on the RAW value, BEFORE any trim: trimming
|
|
1095
|
+
// first would strip the leading and trailing C0 / DEL bytes this is here to
|
|
1096
|
+
// refuse, and a URL whose control character sits at the edge is exactly the
|
|
1097
|
+
// one a parser differential exploits.
|
|
1098
|
+
if (codepointClass.firstControlCharOffset(url, { forbidTab: true }) !== -1) return null;
|
|
1099
|
+
var trimmed = url.trim();
|
|
1100
|
+
if (trimmed.length === 0) return null;
|
|
1101
|
+
if (_isDangerousUrl(trimmed, opts) !== null) return null;
|
|
1102
|
+
for (var i = 0; i < trimmed.length; i += 1) {
|
|
1103
|
+
if (_ATTR_UNSAFE.indexOf(trimmed.charAt(i)) !== -1) return null;
|
|
1104
|
+
}
|
|
1105
|
+
// A relative reference carries no scheme and is fine. A target that DOES
|
|
1106
|
+
// carry one is emitted only when that scheme is on the allowlist: an
|
|
1107
|
+
// unrecognised scheme may still be one the browser hands to a registered
|
|
1108
|
+
// protocol handler, so allowlisting is the only safe direction here.
|
|
1109
|
+
var scheme = _schemeOf(trimmed);
|
|
1110
|
+
if (scheme !== null && RENDER_ALLOWED_SCHEMES.indexOf(scheme) === -1) return null;
|
|
1111
|
+
return _escapeText(trimmed);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
var RENDER_ALLOWED_SCHEMES = ["http", "https", "mailto"];
|
|
1115
|
+
|
|
1116
|
+
// Does the target carry a scheme at all, by the RFC 3986 §3.1 grammar?
|
|
1117
|
+
//
|
|
1118
|
+
// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
|
|
1119
|
+
//
|
|
1120
|
+
// Reading only the leading LETTER run is not that: it stops at the `+` in
|
|
1121
|
+
// `web+evil:` and concludes there is no scheme, so a registered handler scheme
|
|
1122
|
+
// is treated as a relative path and emitted as a live href. The grammar has to
|
|
1123
|
+
// be read whole before a target can be called relative.
|
|
1124
|
+
//
|
|
1125
|
+
// A relative reference never matches, because RFC 3986 §4.2 requires its first
|
|
1126
|
+
// segment to contain no colon - `a+b.c/d` and `./a+b.c` reach a `/` or `.`
|
|
1127
|
+
// that is not part of a scheme, and the walk stops without a colon.
|
|
1128
|
+
function _hasScheme(s) {
|
|
1129
|
+
if (s.length === 0) return false;
|
|
1130
|
+
if (!codepointClass.isAsciiLetter(s.charCodeAt(0))) return false;
|
|
1131
|
+
for (var i = 1; i < s.length; i += 1) {
|
|
1132
|
+
var cc = s.charCodeAt(i);
|
|
1133
|
+
if (cc === 0x3A) return true; // ":" ends the scheme
|
|
1134
|
+
if (codepointClass.isAsciiAlnum(cc)) continue;
|
|
1135
|
+
if (cc === 0x2B || cc === 0x2D || cc === 0x2E) continue; // "+" "-" "."
|
|
1136
|
+
return false; // anything else: no scheme
|
|
1137
|
+
}
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// The scheme text itself, lowercased, for an allowlist comparison. Null when
|
|
1142
|
+
// the target carries no scheme.
|
|
1143
|
+
function _schemeOf(s) {
|
|
1144
|
+
if (!_hasScheme(s)) return null;
|
|
1145
|
+
var end = 0;
|
|
1146
|
+
while (s.charCodeAt(end) !== 0x3A) end += 1;
|
|
1147
|
+
return s.slice(0, end).toLowerCase();
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// ---- Inline ----
|
|
1151
|
+
|
|
1152
|
+
// Emit the inline span `s` (already char-stripped, not yet escaped).
|
|
1153
|
+
// Precedence: code spans first (their contents are literal and are never
|
|
1154
|
+
// re-parsed), then links, then strong, then emphasis.
|
|
1155
|
+
// `rootMatch` and `off` are supplied by the recursive calls below, never by a
|
|
1156
|
+
// caller. The bracket map is derived ONCE for the outermost span and then read
|
|
1157
|
+
// through `_matchIn` at an offset, because both recursion paths hand down a
|
|
1158
|
+
// contiguous slice of the span they were given. Re-deriving it per level made
|
|
1159
|
+
// the work and the live memory scale with nesting DEPTH as well as length:
|
|
1160
|
+
// twenty-four full-length maps for a document inside the balanced profile's
|
|
1161
|
+
// own byte cap, which is how 6.68 MiB of input grew the heap by 1,334 MiB.
|
|
1162
|
+
function _renderInline(s, opts, depth, rootMatch, off) {
|
|
1163
|
+
// Emphasis and link labels recurse. Without a bound, nesting supplied by a
|
|
1164
|
+
// visitor exhausts the call stack, which is a process crash rather than a
|
|
1165
|
+
// refusal - nothing upstream can catch it. Past the bound the span is
|
|
1166
|
+
// emitted as escaped text: the author's characters all survive, they just
|
|
1167
|
+
// stop being markup.
|
|
1168
|
+
var d = depth === undefined ? 0 : depth;
|
|
1169
|
+
if (d > MAX_INLINE_DEPTH) return _escapeText(s);
|
|
1170
|
+
|
|
1171
|
+
var out = "";
|
|
1172
|
+
var i = 0;
|
|
1173
|
+
var textStart = 0;
|
|
1174
|
+
var base = off === undefined ? 0 : off;
|
|
1175
|
+
var match = rootMatch === undefined ? _bracketMap(s) : rootMatch;
|
|
1176
|
+
// One cursor per call, placed at this slice's first opener. The loop below
|
|
1177
|
+
// walks forward only, so the cursor walks with it and every lookup is O(1)
|
|
1178
|
+
// amortised; a nested call gets its own, which costs one search each and is
|
|
1179
|
+
// bounded by MAX_INLINE_DEPTH.
|
|
1180
|
+
var cursor = match ? { i: _lowerBound(match.pos, match.count, base) } : null;
|
|
1181
|
+
function flush(upTo) { out += _escapeText(s.slice(textStart, upTo)); }
|
|
1182
|
+
|
|
1183
|
+
while (i < s.length) {
|
|
1184
|
+
var ch = s.charAt(i);
|
|
1185
|
+
|
|
1186
|
+
// Backslash escape: the next character is literal, never a delimiter.
|
|
1187
|
+
if (ch === "\\" && i + 1 < s.length) {
|
|
1188
|
+
flush(i);
|
|
1189
|
+
out += _escapeText(s.charAt(i + 1));
|
|
1190
|
+
i += 2;
|
|
1191
|
+
textStart = i;
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Code span - contents literal. The opening run is capped so a line of
|
|
1196
|
+
// nothing but backticks cannot make the fence length itself O(n).
|
|
1197
|
+
if (ch === "`") {
|
|
1198
|
+
var tickRun = _runLength(s, i, "`", MAX_DELIMITER_RUN);
|
|
1199
|
+
var close = _findRun(s, i + tickRun, "`", tickRun);
|
|
1200
|
+
if (close !== -1) {
|
|
1201
|
+
flush(i);
|
|
1202
|
+
_charge(13); // <code></code>
|
|
1203
|
+
out += "<code>" + _escapeText(s.slice(i + tickRun, close)) + "</code>";
|
|
1204
|
+
i = close + tickRun;
|
|
1205
|
+
textStart = i;
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Image - outside the subset. Emitted as escaped text so an
|
|
1211
|
+
// author-controlled src never becomes an element attribute.
|
|
1212
|
+
if (ch === "!" && s.charAt(i + 1) === "[") {
|
|
1213
|
+
var img = _parseLink(s, i + 1, match, base, cursor);
|
|
1214
|
+
if (img !== null) {
|
|
1215
|
+
flush(i);
|
|
1216
|
+
out += _escapeText(s.slice(i, img.end));
|
|
1217
|
+
i = img.end;
|
|
1218
|
+
textStart = i;
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Link.
|
|
1224
|
+
if (ch === "[") {
|
|
1225
|
+
var link = _parseLink(s, i, match, base, cursor);
|
|
1226
|
+
if (link !== null) {
|
|
1227
|
+
flush(i);
|
|
1228
|
+
var href = _safeHref(link.url, opts);
|
|
1229
|
+
// link.text is s.slice(i + 1, ...), so it starts one past the "[".
|
|
1230
|
+
var label = _renderInline(link.text, opts, d + 1, match, base + i + 1);
|
|
1231
|
+
// A refused target still shows the author's words - dropping them
|
|
1232
|
+
// would hide content rather than neutralise a link.
|
|
1233
|
+
// The fixed wrapper only. `label` is author text charged as it was
|
|
1234
|
+
// escaped, and `href` came back from _safeHref THROUGH _escapeText, so
|
|
1235
|
+
// it was charged there too — subtracting only the label would bill the
|
|
1236
|
+
// target twice and refuse links whose real output is well inside the
|
|
1237
|
+
// bound.
|
|
1238
|
+
if (href !== null) _charge(50); // <a href="" rel="...">…</a>
|
|
1239
|
+
out += href === null ? label
|
|
1240
|
+
: '<a href="' + href + '" rel="nofollow noopener noreferrer">' + label + "</a>";
|
|
1241
|
+
i = link.end;
|
|
1242
|
+
textStart = i;
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Strong, then emphasis. Both delimiters, longest run first. Only the
|
|
1248
|
+
// first two characters of the run decide which, so that is all we count.
|
|
1249
|
+
if (ch === "*" || ch === "_") {
|
|
1250
|
+
var run = _runLength(s, i, ch, 2);
|
|
1251
|
+
var want = run >= 2 ? 2 : 1;
|
|
1252
|
+
var end = _findRun(s, i + want, ch, want);
|
|
1253
|
+
if (end !== -1 && end > i + want) {
|
|
1254
|
+
flush(i);
|
|
1255
|
+
var innerHtml = _renderInline(s.slice(i + want, end), opts, d + 1,
|
|
1256
|
+
match, base + i + want);
|
|
1257
|
+
_charge(want === 2 ? 17 : 9); // <strong></strong> | <em></em>
|
|
1258
|
+
out += want === 2 ? "<strong>" + innerHtml + "</strong>"
|
|
1259
|
+
: "<em>" + innerHtml + "</em>";
|
|
1260
|
+
i = end + want;
|
|
1261
|
+
textStart = i;
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
i += 1;
|
|
1267
|
+
}
|
|
1268
|
+
flush(s.length);
|
|
1269
|
+
return out;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// Length of the run of `ch` starting at `at`, counted no further than `cap`.
|
|
1273
|
+
// The cap is what keeps delimiter scanning linear: every caller only needs to
|
|
1274
|
+
// know whether the run reaches some small length, and walking the whole run to
|
|
1275
|
+
// answer that turns an input of 20k repeated asterisks into a quadratic scan
|
|
1276
|
+
// (measured at ~1s before the cap went in).
|
|
1277
|
+
function _runLength(s, at, ch, cap) {
|
|
1278
|
+
var n = 0;
|
|
1279
|
+
var limit = cap === undefined ? s.length : cap;
|
|
1280
|
+
while (n < limit && at + n < s.length && s.charAt(at + n) === ch) n += 1;
|
|
1281
|
+
return n;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// Index of the next run of at least `n` `ch` at or after `from`, or -1.
|
|
1285
|
+
function _findRun(s, from, ch, n) {
|
|
1286
|
+
for (var i = from; i < s.length; i += 1) {
|
|
1287
|
+
if (s.charAt(i) !== ch) continue;
|
|
1288
|
+
if (_runLength(s, i, ch, n) < n) continue;
|
|
1289
|
+
return i;
|
|
1290
|
+
}
|
|
1291
|
+
return -1;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// Match every `[` to its `]` and every `(` to its `)` in ONE pass, so a link
|
|
1295
|
+
// parse is a lookup rather than a forward scan. Scanning per delimiter is what
|
|
1296
|
+
// makes an input of 50k unmatched brackets quadratic (measured at ~1.7s before
|
|
1297
|
+
// this map existed); an unmatched opener simply has no entry.
|
|
1298
|
+
// Counts calls, so a test can assert that a document's nesting DEPTH does not
|
|
1299
|
+
// multiply the number of maps built. A time or heap assertion would say the
|
|
1300
|
+
// same thing with a threshold that has to be tuned per machine and flakes
|
|
1301
|
+
// under a loaded runner; the allocation count is exact and depth-independent.
|
|
1302
|
+
var _bracketMapsBuilt = 0;
|
|
1303
|
+
var _bracketArraysAllocated = 0;
|
|
1304
|
+
var _bracketIndexEntries = 0;
|
|
1305
|
+
// Counts index positions examined, so a test can assert the scan stays linear
|
|
1306
|
+
// in the delimiters. A wall-clock assertion would say the same thing with a
|
|
1307
|
+
// threshold that moves with the machine and flakes under a loaded runner.
|
|
1308
|
+
var _bracketLookupSteps = 0;
|
|
1309
|
+
// One per top-level block render. Nesting must not add more - that is the
|
|
1310
|
+
// difference between recording where a quoted line starts and copying it.
|
|
1311
|
+
var _blockOffsetArrays = 0;
|
|
1312
|
+
|
|
1313
|
+
// Three arrays here are sized by the document, and none of them is allocated
|
|
1314
|
+
// until a character needs it.
|
|
1315
|
+
//
|
|
1316
|
+
// A dense `new Array(s.length)` filled with -1 cost one slot per CHARACTER
|
|
1317
|
+
// whatever the document contained, so 64 MiB of ordinary prose carrying no
|
|
1318
|
+
// brackets at all still built 67 million slots before a word was rendered.
|
|
1319
|
+
// Concurrent requests inside the advertised byte cap could exhaust memory on
|
|
1320
|
+
// input with nothing hostile in it.
|
|
1321
|
+
//
|
|
1322
|
+
// Int32Array rather than a plain Array: four bytes a slot instead of eight,
|
|
1323
|
+
// and the platform zeroes it, so there is no O(n) fill loop in JS either. Zero
|
|
1324
|
+
// doubles as "no match" - a closer always sits after its opener, so a recorded
|
|
1325
|
+
// match is never position 0 and the sentinel cannot collide with a real one.
|
|
1326
|
+
//
|
|
1327
|
+
// The opener stacks get the same treatment. They are the reason a run of
|
|
1328
|
+
// unmatched `[` was expensive even though it produces no pairs: every opener
|
|
1329
|
+
// was pushed and none popped. Measured on 8 MiB of `[`, the three-array shape
|
|
1330
|
+
// costs 32 MiB where the plain-array one cost 221 MiB.
|
|
1331
|
+
function _bracketMap(s) {
|
|
1332
|
+
_bracketMapsBuilt += 1;
|
|
1333
|
+
var n = s.length;
|
|
1334
|
+
var i, c;
|
|
1335
|
+
|
|
1336
|
+
// Pass one counts the openers. Nothing here is sized by the DOCUMENT, only
|
|
1337
|
+
// by the delimiters actually in it, so a 64 MiB document carrying a single
|
|
1338
|
+
// "[" pays for one entry rather than for sixty-seven million. Counting
|
|
1339
|
+
// first also means no growth-and-copy: pass two allocates exactly what pass
|
|
1340
|
+
// one found. The escape rule has to be identical in both passes or the
|
|
1341
|
+
// counts and the fill disagree.
|
|
1342
|
+
var nSquare = 0, nRound = 0;
|
|
1343
|
+
for (i = 0; i < n; i += 1) {
|
|
1344
|
+
c = s.charAt(i);
|
|
1345
|
+
if (c === "\\") { i += 1; continue; }
|
|
1346
|
+
if (c === "[") nSquare += 1;
|
|
1347
|
+
else if (c === "(") nRound += 1;
|
|
1348
|
+
}
|
|
1349
|
+
var nOpen = nSquare + nRound;
|
|
1350
|
+
if (nOpen === 0) return null;
|
|
1351
|
+
// A delimiter cap, separate from the byte and line caps.
|
|
1352
|
+
//
|
|
1353
|
+
// The three arrays below are sized by the openers, which is the right unit -
|
|
1354
|
+
// but a document that is NOTHING but openers has as many of them as it has
|
|
1355
|
+
// characters, so at the permissive profile's 64 MiB they would still reserve
|
|
1356
|
+
// hundreds of megabytes. Byte and line caps do not bound this, because a
|
|
1357
|
+
// single 64 MiB line of "[" satisfies both.
|
|
1358
|
+
//
|
|
1359
|
+
// Refused rather than degraded: silently ignoring delimiters past a bound
|
|
1360
|
+
// would make link detection depend on how far into the document a link sits,
|
|
1361
|
+
// which is worse than saying no. The counting pass above is O(1) in memory,
|
|
1362
|
+
// so the refusal happens BEFORE anything document-scale is allocated. Two
|
|
1363
|
+
// million openers is far past any real document - a 64 MiB file would need a
|
|
1364
|
+
// bracket every 32 bytes - and bounds the index at about 24 MiB.
|
|
1365
|
+
if (nOpen > MAX_INLINE_DELIMITERS) {
|
|
1366
|
+
throw _err("markdown/too-many-delimiters",
|
|
1367
|
+
"b.guardMarkdown.render: source contains " + nOpen + " bracket delimiters, " +
|
|
1368
|
+
"over the " + MAX_INLINE_DELIMITERS + " the inline index will hold");
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// `pos` holds opener positions in ascending order, which they naturally are
|
|
1372
|
+
// - the scan runs left to right - so a position can be found by binary
|
|
1373
|
+
// search rather than by indexing an array the size of the text. `mate` holds
|
|
1374
|
+
// each opener's closer, keyed by the same rank. Zero means unmatched: a
|
|
1375
|
+
// closer always sits after its opener, so a real one is never position 0.
|
|
1376
|
+
var pos = new Int32Array(nOpen);
|
|
1377
|
+
var mate = new Int32Array(nOpen);
|
|
1378
|
+
var sqStack = new Int32Array(nSquare);
|
|
1379
|
+
var rdStack = new Int32Array(nRound);
|
|
1380
|
+
_bracketArraysAllocated += 1;
|
|
1381
|
+
_bracketIndexEntries = nOpen;
|
|
1382
|
+
|
|
1383
|
+
var sqTop = 0, rdTop = 0, k = 0;
|
|
1384
|
+
for (i = 0; i < n; i += 1) {
|
|
1385
|
+
c = s.charAt(i);
|
|
1386
|
+
if (c === "\\") { i += 1; continue; }
|
|
1387
|
+
if (c === "[") { pos[k] = i; sqStack[sqTop] = k; sqTop += 1; k += 1; }
|
|
1388
|
+
else if (c === "(") { pos[k] = i; rdStack[rdTop] = k; rdTop += 1; k += 1; }
|
|
1389
|
+
else if (c === "]") { if (sqTop > 0) { sqTop -= 1; mate[sqStack[sqTop]] = i; } }
|
|
1390
|
+
else if (c === ")") { if (rdTop > 0) { rdTop -= 1; mate[rdStack[rdTop]] = i; } }
|
|
1391
|
+
}
|
|
1392
|
+
return { pos: pos, mate: mate, count: k };
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// Read one entry of the root map through the window a nested span occupies.
|
|
1396
|
+
//
|
|
1397
|
+
// The map holds absolute positions in the outermost span; `z` is relative to
|
|
1398
|
+
// the slice being rendered. A partner outside the slice reads as no partner,
|
|
1399
|
+
// which is the same answer a map built for the slice alone would give: it
|
|
1400
|
+
// never saw the character that closes the pair. `*a [b* c]` is the shape -
|
|
1401
|
+
// the emphasis run ends before the bracket's partner, so inside the emphasis
|
|
1402
|
+
// the `[` is unmatched either way.
|
|
1403
|
+
// Rank of the first opener at or after `target`.
|
|
1404
|
+
function _lowerBound(pos, count, target) {
|
|
1405
|
+
var lo = 0;
|
|
1406
|
+
var hi = count;
|
|
1407
|
+
while (lo < hi) {
|
|
1408
|
+
var mid = (lo + hi) >> 1;
|
|
1409
|
+
if (pos[mid] < target) lo = mid + 1;
|
|
1410
|
+
else hi = mid;
|
|
1411
|
+
}
|
|
1412
|
+
return lo;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// Resolve the closer for the opener at slice-relative `z`.
|
|
1416
|
+
//
|
|
1417
|
+
// `cursor` is the scan position of the caller that is walking this slice left
|
|
1418
|
+
// to right. Because that walk only ever moves forward, the cursor advances
|
|
1419
|
+
// with it and each lookup costs O(1) amortised - the whole scan stays linear
|
|
1420
|
+
// in the delimiters, which is what the profile documentation promises. A
|
|
1421
|
+
// lookup with no cursor falls back to a search, for the one caller that asks
|
|
1422
|
+
// about a position the main scan has already passed.
|
|
1423
|
+
function _matchIn(match, base, len, z, cursor) {
|
|
1424
|
+
// No map at all means the document held no openers - nothing matches.
|
|
1425
|
+
if (match === null || match === undefined) return -1;
|
|
1426
|
+
var target = z + base;
|
|
1427
|
+
var at;
|
|
1428
|
+
if (cursor) {
|
|
1429
|
+
while (cursor.i < match.count && match.pos[cursor.i] < target) {
|
|
1430
|
+
cursor.i += 1;
|
|
1431
|
+
_bracketLookupSteps += 1;
|
|
1432
|
+
}
|
|
1433
|
+
at = cursor.i;
|
|
1434
|
+
} else {
|
|
1435
|
+
at = _lowerBound(match.pos, match.count, target);
|
|
1436
|
+
_bracketLookupSteps += 1;
|
|
1437
|
+
}
|
|
1438
|
+
if (at >= match.count || match.pos[at] !== target) return -1;
|
|
1439
|
+
var m = match.mate[at];
|
|
1440
|
+
if (m === 0) return -1; // recorded no closer
|
|
1441
|
+
m -= base;
|
|
1442
|
+
return (m >= 0 && m < len) ? m : -1;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// Parse `[text](url)` starting at the `[`. Returns { text, url, end } or null.
|
|
1446
|
+
// Nesting inside the label and inside the target is handled by the match map,
|
|
1447
|
+
// so `[a [b] c](u)` and `(a(b))` close where a reader expects.
|
|
1448
|
+
function _parseLink(s, at, match, base, cursor) {
|
|
1449
|
+
if (s.charAt(at) !== "[") return null;
|
|
1450
|
+
var off = base === undefined ? 0 : base;
|
|
1451
|
+
var textEnd = _matchIn(match, off, s.length, at, cursor);
|
|
1452
|
+
if (textEnd === -1 || s.charAt(textEnd + 1) !== "(") return null;
|
|
1453
|
+
// No cursor for the target's opening paren. It sits past the position the
|
|
1454
|
+
// caller's scan has reached, and moving the shared cursor there would skip
|
|
1455
|
+
// over openers between the two that the scan has still to visit - if this
|
|
1456
|
+
// parse then fails, those would never be found.
|
|
1457
|
+
var urlEnd = _matchIn(match, off, s.length, textEnd + 1, null);
|
|
1458
|
+
if (urlEnd === -1) return null;
|
|
1459
|
+
var target = s.slice(textEnd + 2, urlEnd);
|
|
1460
|
+
// A title after the target ("url \"title\"") is outside the subset; keep the
|
|
1461
|
+
// target and drop the rest rather than emitting an unvalidated attribute.
|
|
1462
|
+
var sp = _firstSpace(target);
|
|
1463
|
+
if (sp !== -1) target = target.slice(0, sp);
|
|
1464
|
+
return { text: s.slice(at + 1, textEnd), url: target, end: urlEnd + 1 };
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// ASCII space, tab, and the line terminators - NOT the broad Unicode
|
|
1468
|
+
// whitespace set. A NO-BREAK SPACE is content in Markdown, and treating it as
|
|
1469
|
+
// blank would let U+00A0 end a paragraph or empty a list item.
|
|
1470
|
+
function _isMdSpace(cc) {
|
|
1471
|
+
return cc === 0x20 || (cc >= 0x09 && cc <= 0x0D);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function _firstSpace(s) {
|
|
1475
|
+
for (var i = 0; i < s.length; i += 1) {
|
|
1476
|
+
if (_isMdSpace(s.charCodeAt(i))) return i;
|
|
1477
|
+
}
|
|
1478
|
+
return -1;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// ---- Block ----
|
|
1482
|
+
|
|
1483
|
+
function _leadingSpaces(line) {
|
|
1484
|
+
var n = 0;
|
|
1485
|
+
while (n < line.length && (line.charAt(n) === " " || line.charAt(n) === "\t")) n += 1;
|
|
1486
|
+
return n;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function _isBlank(line) {
|
|
1490
|
+
for (var i = 0; i < line.length; i += 1) {
|
|
1491
|
+
if (!_isMdSpace(line.charCodeAt(i))) return false;
|
|
1492
|
+
}
|
|
1493
|
+
return true;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// `---`, `***` or `___`, three or more, nothing else on the line.
|
|
1497
|
+
function _isThematicBreak(line) {
|
|
1498
|
+
var t = line.trim();
|
|
1499
|
+
if (t.length < 3) return false;
|
|
1500
|
+
var ch = t.charAt(0);
|
|
1501
|
+
if (ch !== "-" && ch !== "*" && ch !== "_") return false;
|
|
1502
|
+
for (var i = 0; i < t.length; i += 1) if (t.charAt(i) !== ch) return false;
|
|
1503
|
+
return true;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// A bullet marker: "-", "*" or "+" followed by a space.
|
|
1507
|
+
function _bulletAt(line) {
|
|
1508
|
+
var n = _leadingSpaces(line);
|
|
1509
|
+
var ch = line.charAt(n);
|
|
1510
|
+
if (ch !== "-" && ch !== "*" && ch !== "+") return -1;
|
|
1511
|
+
if (line.charAt(n + 1) !== " ") return -1;
|
|
1512
|
+
return n + 2;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// An ordered marker: digits followed by "." or ")" and a space.
|
|
1516
|
+
function _orderedAt(line) {
|
|
1517
|
+
var n = _leadingSpaces(line);
|
|
1518
|
+
var d = n;
|
|
1519
|
+
while (d < line.length && codepointClass.isAsciiDigit(line.charCodeAt(d))) d += 1;
|
|
1520
|
+
if (d === n) return -1;
|
|
1521
|
+
var sep = line.charAt(d);
|
|
1522
|
+
if (sep !== "." && sep !== ")") return -1;
|
|
1523
|
+
if (line.charAt(d + 1) !== " ") return -1;
|
|
1524
|
+
return d + 2;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// A fence opener: three or more backticks or tildes at the line start.
|
|
1528
|
+
function _fenceAt(line) {
|
|
1529
|
+
var n = _leadingSpaces(line);
|
|
1530
|
+
var ch = line.charAt(n);
|
|
1531
|
+
if (ch !== "`" && ch !== "~") return null;
|
|
1532
|
+
var run = _runLength(line, n, ch);
|
|
1533
|
+
if (run < 3) return null;
|
|
1534
|
+
return { ch: ch, run: run, info: line.slice(n + run).trim() };
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
var MAX_HEADING_LEVEL = 6;
|
|
1538
|
+
|
|
1539
|
+
// An ATX heading, or null. `#` alone is not one, `#no-space` is not one, and
|
|
1540
|
+
// seven hashes is not one - each of those is ordinary text.
|
|
1541
|
+
//
|
|
1542
|
+
// This is a NAMED predicate rather than an inline test because the block
|
|
1543
|
+
// dispatcher and the paragraph terminator both have to agree on what starts a
|
|
1544
|
+
// heading. When they were written separately they disagreed: the paragraph
|
|
1545
|
+
// loop stopped at any line beginning with `#` while the dispatcher accepted
|
|
1546
|
+
// only a well-formed one, so `#not-a-heading` matched no block, ended no
|
|
1547
|
+
// paragraph, and was dropped without a trace.
|
|
1548
|
+
function _headingAt(line) {
|
|
1549
|
+
var at = _leadingSpaces(line);
|
|
1550
|
+
if (line.charAt(at) !== "#") return null;
|
|
1551
|
+
var level = _runLength(line, at, "#", MAX_HEADING_LEVEL + 1);
|
|
1552
|
+
if (level > MAX_HEADING_LEVEL) return null;
|
|
1553
|
+
if (at + level !== line.length && line.charAt(at + level) !== " ") return null;
|
|
1554
|
+
var text = line.slice(at + level).trim();
|
|
1555
|
+
// Trailing closing hashes are decoration, not content.
|
|
1556
|
+
while (text.length > 0 && text.charAt(text.length - 1) === "#") {
|
|
1557
|
+
text = text.slice(0, text.length - 1);
|
|
1558
|
+
}
|
|
1559
|
+
return { level: level, text: text.trim() };
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// Four spaces (or a tab) of indent opens a code block - but only where a
|
|
1563
|
+
// paragraph or list item is not already in progress, which the caller decides.
|
|
1564
|
+
var INDENTED_CODE_COLUMNS = 4;
|
|
1565
|
+
|
|
1566
|
+
function _isIndentedCode(line) {
|
|
1567
|
+
if (_isBlank(line)) return false;
|
|
1568
|
+
if (line.charAt(0) === "\t") return true;
|
|
1569
|
+
for (var i = 0; i < INDENTED_CODE_COLUMNS; i += 1) {
|
|
1570
|
+
if (line.charAt(i) !== " ") return false;
|
|
1571
|
+
}
|
|
1572
|
+
return true;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// The ONE answer to "does this line start a block?", shared by the dispatcher
|
|
1576
|
+
// and the paragraph terminator so the two cannot drift apart again.
|
|
1577
|
+
function _startsBlock(line) {
|
|
1578
|
+
return _fenceAt(line) !== null ||
|
|
1579
|
+
_isThematicBreak(line) ||
|
|
1580
|
+
_headingAt(line) !== null ||
|
|
1581
|
+
line.charAt(_leadingSpaces(line)) === ">" ||
|
|
1582
|
+
_bulletAt(line) !== -1 ||
|
|
1583
|
+
_orderedAt(line) !== -1;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// How deep emphasis and link labels may nest before the span is emitted as
|
|
1587
|
+
// escaped text instead of markup. Well past any hand-written document, far
|
|
1588
|
+
// short of the call-stack limit.
|
|
1589
|
+
var MAX_INLINE_DEPTH = 24;
|
|
1590
|
+
|
|
1591
|
+
// The blockquote renderer recurses once per nesting level, so the depth it can
|
|
1592
|
+
// survive is a property of the call stack, not of operator policy.
|
|
1593
|
+
// maxBlockquoteDepth is an operator-settable cap and the profiles set it to
|
|
1594
|
+
// 16 / 64 / 256; nothing stopped a caller passing 10001, at which point a
|
|
1595
|
+
// document of 10,000 "> " prefixes exhausted the stack and threw a native
|
|
1596
|
+
// RangeError - a process-level failure escaping a primitive whose whole job is
|
|
1597
|
+
// to turn hostile input into a refusal. This ceiling sits above the loosest
|
|
1598
|
+
// shipped profile, so it changes no profile's behaviour; it only bounds what a
|
|
1599
|
+
// raised policy limit can ask the implementation to do.
|
|
1600
|
+
var MAX_BLOCKQUOTE_RECURSION = 512;
|
|
1601
|
+
|
|
1602
|
+
// The most bracket delimiters the inline index will hold. See _bracketMap: the
|
|
1603
|
+
// index is sized by openers, so a document made entirely of them is sized by
|
|
1604
|
+
// its length however small the byte cap is set. Two million bounds the index
|
|
1605
|
+
// at roughly 24 MiB and sits far past any real document.
|
|
1606
|
+
var MAX_INLINE_DELIMITERS = 2000000;
|
|
1607
|
+
|
|
1608
|
+
// Longest delimiter run any caller needs to distinguish. Runs longer than this
|
|
1609
|
+
// are still matched; only the COUNTING stops here, which is what keeps a line
|
|
1610
|
+
// of repeated delimiters linear rather than quadratic.
|
|
1611
|
+
var MAX_DELIMITER_RUN = 8;
|
|
1612
|
+
|
|
1613
|
+
// `src`/`off`/`from`/`to` are supplied by the blockquote recursion below and
|
|
1614
|
+
// never by a caller. Stripping a quote level used to build a fresh array of
|
|
1615
|
+
// fresh strings for every line in the run, at every level, with each parent's
|
|
1616
|
+
// array still live - so a document nested to the profile's permitted depth
|
|
1617
|
+
// retained roughly depth x length. Measured at the permissive profile, 3.94 MiB
|
|
1618
|
+
// nested 256 deep grew the heap by 85 MiB, and the cap allows sixteen times
|
|
1619
|
+
// that input.
|
|
1620
|
+
//
|
|
1621
|
+
// Now one offsets array is allocated for the whole document and a level records
|
|
1622
|
+
// where each line's content starts, so a level costs the offsets it advances
|
|
1623
|
+
// rather than a copy of everything below it.
|
|
1624
|
+
function _renderBlocks(lines, opts, depth, off, from, to) {
|
|
1625
|
+
var d = depth === undefined ? 0 : depth;
|
|
1626
|
+
var lo = from === undefined ? 0 : from;
|
|
1627
|
+
var hi = to === undefined ? lines.length : to;
|
|
1628
|
+
var offs = off;
|
|
1629
|
+
if (offs === undefined) { offs = new Int32Array(lines.length); _blockOffsetArrays += 1; }
|
|
1630
|
+
// Reading a line applies its recorded offset. V8 shares the backing store for
|
|
1631
|
+
// a slice of this size, so this is a view rather than a copy of the text.
|
|
1632
|
+
function L(k) {
|
|
1633
|
+
var o = offs[k];
|
|
1634
|
+
return o === 0 ? lines[k] : lines[k].slice(o);
|
|
1635
|
+
}
|
|
1636
|
+
var out = "";
|
|
1637
|
+
var i = lo;
|
|
1638
|
+
while (i < hi) {
|
|
1639
|
+
var line = L(i);
|
|
1640
|
+
|
|
1641
|
+
if (_isBlank(line)) { i += 1; continue; }
|
|
1642
|
+
|
|
1643
|
+
var fence = _fenceAt(line);
|
|
1644
|
+
if (fence !== null) {
|
|
1645
|
+
var body = [];
|
|
1646
|
+
i += 1;
|
|
1647
|
+
while (i < hi) {
|
|
1648
|
+
var f = _fenceAt(L(i));
|
|
1649
|
+
if (f !== null && f.ch === fence.ch && f.run >= fence.run && f.info === "") { i += 1; break; }
|
|
1650
|
+
body.push(L(i));
|
|
1651
|
+
i += 1;
|
|
1652
|
+
}
|
|
1653
|
+
// The info string is author-controlled, so it becomes an escaped class
|
|
1654
|
+
// rather than being interpolated raw.
|
|
1655
|
+
var cls = fence.info.length > 0
|
|
1656
|
+
? ' class="language-' + _escapeText(_firstWord(fence.info)) + '"' : "";
|
|
1657
|
+
// The fixed markup only. `cls` embeds _escapeText(_firstWord(info)), so
|
|
1658
|
+
// the info token was already charged as it was escaped; adding cls.length
|
|
1659
|
+
// would bill it twice.
|
|
1660
|
+
_charge(fence.info.length > 0 ? 43 : 25); // + ` class="language-"`
|
|
1661
|
+
out += "<pre><code" + cls + ">" + _escapeText(body.join("\n")) + "</code></pre>\n";
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
if (_isThematicBreak(line)) { _charge(5); out += "<hr>\n"; i += 1; continue; }
|
|
1666
|
+
|
|
1667
|
+
// ATX heading.
|
|
1668
|
+
var heading = _headingAt(line);
|
|
1669
|
+
if (heading !== null) {
|
|
1670
|
+
_charge(9); // <hN></hN>\n
|
|
1671
|
+
out += "<h" + heading.level + ">" + _renderInline(heading.text, opts) +
|
|
1672
|
+
"</h" + heading.level + ">\n";
|
|
1673
|
+
i += 1;
|
|
1674
|
+
continue;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// Indented code - four spaces or a tab, contents literal. Only reachable
|
|
1678
|
+
// where no paragraph is in progress, because the paragraph loop below
|
|
1679
|
+
// consumes its own continuation lines first.
|
|
1680
|
+
if (_isIndentedCode(line)) {
|
|
1681
|
+
var codeLines = [];
|
|
1682
|
+
while (i < hi && (_isIndentedCode(L(i)) || _isBlank(L(i)))) {
|
|
1683
|
+
// A blank line inside indented code belongs to the block only if more
|
|
1684
|
+
// indented code follows; otherwise it ends it.
|
|
1685
|
+
if (_isBlank(L(i))) {
|
|
1686
|
+
var j = i + 1;
|
|
1687
|
+
while (j < hi && _isBlank(L(j))) j += 1;
|
|
1688
|
+
if (j >= hi || !_isIndentedCode(L(j))) break;
|
|
1689
|
+
codeLines.push("");
|
|
1690
|
+
i += 1;
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
var codeLine = L(i);
|
|
1694
|
+
codeLines.push(codeLine.charAt(0) === "\t"
|
|
1695
|
+
? codeLine.slice(1)
|
|
1696
|
+
: codeLine.slice(INDENTED_CODE_COLUMNS));
|
|
1697
|
+
i += 1;
|
|
1698
|
+
}
|
|
1699
|
+
_charge(25); // <pre><code></code></pre>\n
|
|
1700
|
+
out += "<pre><code>" + _escapeText(codeLines.join("\n")) + "</code></pre>\n";
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// Blockquote - collect the run, strip one "> " level, recurse. The depth
|
|
1705
|
+
// is bounded by the profile's own maxBlockquoteDepth, which validate
|
|
1706
|
+
// already enforces; without it here, nesting supplied by a visitor
|
|
1707
|
+
// exhausts the call stack and takes the process down rather than
|
|
1708
|
+
// producing a refusal something upstream could handle.
|
|
1709
|
+
if (line.charAt(_leadingSpaces(line)) === ">") {
|
|
1710
|
+
var bqCap = opts.maxBlockquoteDepth < MAX_BLOCKQUOTE_RECURSION
|
|
1711
|
+
? opts.maxBlockquoteDepth
|
|
1712
|
+
: MAX_BLOCKQUOTE_RECURSION;
|
|
1713
|
+
if (d >= bqCap) {
|
|
1714
|
+
throw _err("markdown/blockquote-depth",
|
|
1715
|
+
"b.guardMarkdown.render: blockquote nesting exceeds " +
|
|
1716
|
+
(bqCap === opts.maxBlockquoteDepth
|
|
1717
|
+
? "maxBlockquoteDepth (" + opts.maxBlockquoteDepth + ")"
|
|
1718
|
+
: "the " + MAX_BLOCKQUOTE_RECURSION + "-level renderer ceiling, below " +
|
|
1719
|
+
"the configured maxBlockquoteDepth of " + opts.maxBlockquoteDepth));
|
|
1720
|
+
}
|
|
1721
|
+
// Record where each line's content starts one level in, rather than
|
|
1722
|
+
// building a level's worth of new strings. The run is a contiguous window
|
|
1723
|
+
// of the SAME line array, so the recursion needs a window rather than a
|
|
1724
|
+
// copy of it.
|
|
1725
|
+
var qStart = i;
|
|
1726
|
+
while (i < hi) {
|
|
1727
|
+
var ql = L(i);
|
|
1728
|
+
if (_isBlank(ql) || ql.charAt(_leadingSpaces(ql)) !== ">") break;
|
|
1729
|
+
var adv = _leadingSpaces(ql) + 1;
|
|
1730
|
+
if (ql.charAt(adv) === " ") adv += 1;
|
|
1731
|
+
offs[i] += adv;
|
|
1732
|
+
i += 1;
|
|
1733
|
+
}
|
|
1734
|
+
_charge(27); // <blockquote>\n</blockquote>\n
|
|
1735
|
+
out += "<blockquote>\n" + _renderBlocks(lines, opts, d + 1, offs, qStart, i) +
|
|
1736
|
+
"</blockquote>\n";
|
|
1737
|
+
continue;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// Lists.
|
|
1741
|
+
var bullet = _bulletAt(line);
|
|
1742
|
+
var ordered = bullet === -1 ? _orderedAt(line) : -1;
|
|
1743
|
+
if (bullet !== -1 || ordered !== -1) {
|
|
1744
|
+
var isOrdered = bullet === -1;
|
|
1745
|
+
var tag = isOrdered ? "ol" : "ul";
|
|
1746
|
+
_charge(tag.length + 3); // <ul>\n | <ol>\n
|
|
1747
|
+
out += "<" + tag + ">\n";
|
|
1748
|
+
while (i < hi) {
|
|
1749
|
+
var listLine = L(i);
|
|
1750
|
+
var at = isOrdered ? _orderedAt(listLine) : _bulletAt(listLine);
|
|
1751
|
+
if (at === -1) break;
|
|
1752
|
+
_charge(10); // <li></li>\n
|
|
1753
|
+
out += "<li>" + _renderInline(listLine.slice(at).trim(), opts) + "</li>\n";
|
|
1754
|
+
i += 1;
|
|
1755
|
+
}
|
|
1756
|
+
_charge(tag.length + 4); // </ul>\n | </ol>\n
|
|
1757
|
+
out += "</" + tag + ">\n";
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// Paragraph - this line plus every following non-blank line that starts no
|
|
1762
|
+
// other block. The FIRST line is taken unconditionally: control only
|
|
1763
|
+
// reaches here when no block matched it, so testing it again could only
|
|
1764
|
+
// disagree with the dispatcher, which is how lines went missing before.
|
|
1765
|
+
var para = [line.trim()];
|
|
1766
|
+
i += 1;
|
|
1767
|
+
while (i < hi) {
|
|
1768
|
+
var paraLine = L(i);
|
|
1769
|
+
if (_isBlank(paraLine) || _startsBlock(paraLine)) break;
|
|
1770
|
+
para.push(paraLine.trim());
|
|
1771
|
+
i += 1;
|
|
1772
|
+
}
|
|
1773
|
+
_charge(8); // <p></p>\n
|
|
1774
|
+
out += "<p>" + _renderInline(para.join("\n"), opts) + "</p>\n";
|
|
1775
|
+
}
|
|
1776
|
+
return out;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function _firstWord(s) {
|
|
1780
|
+
var sp = _firstSpace(s);
|
|
1781
|
+
return sp === -1 ? s : s.slice(0, sp);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
/**
|
|
1785
|
+
* @primitive b.guardMarkdown.render
|
|
1786
|
+
* @signature b.guardMarkdown.render(source, opts?)
|
|
1787
|
+
* @since 0.18.44
|
|
1788
|
+
* @status stable
|
|
1789
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
1790
|
+
* @related b.guardMarkdown.validate, b.guardMarkdown.sanitize, b.template.escapeHtml
|
|
1791
|
+
*
|
|
1792
|
+
* Render Markdown to an HTML fragment, escaping by default.
|
|
1793
|
+
*
|
|
1794
|
+
* Every text node leaves through the shared markup escaper, every link target
|
|
1795
|
+
* is screened before it can become an `href`, and raw HTML is emitted as
|
|
1796
|
+
* escaped text rather than passed through. Those are the three things a
|
|
1797
|
+
* hand-rolled emitter gets wrong, and each of them is a stored-XSS hole
|
|
1798
|
+
* wherever author-supplied prose is shown to a visitor.
|
|
1799
|
+
*
|
|
1800
|
+
* The subset is deliberate: paragraphs, ATX headings, bullet and ordered
|
|
1801
|
+
* lists, fenced and indented code, blockquotes, thematic breaks, emphasis,
|
|
1802
|
+
* strong, code spans and links. Anything outside it - images, tables,
|
|
1803
|
+
* reference links, footnotes, raw HTML - renders as escaped text. That is a
|
|
1804
|
+
* display limitation by choice: an unrecognised construct that shows its own
|
|
1805
|
+
* source is a formatting bug, while one that becomes markup is a
|
|
1806
|
+
* vulnerability.
|
|
1807
|
+
*
|
|
1808
|
+
* Link targets are limited to `http`, `https`, `mailto` and relative
|
|
1809
|
+
* references. A target carrying any other scheme, an attribute-breaking
|
|
1810
|
+
* character, or a control character is refused - the link's TEXT is still
|
|
1811
|
+
* rendered, so a refusal never silently deletes the author's words. Anchors
|
|
1812
|
+
* carry `rel="nofollow noopener noreferrer"`.
|
|
1813
|
+
*
|
|
1814
|
+
* BIDI, zero-width, C0-control and NUL characters are stripped before parsing
|
|
1815
|
+
* regardless of profile. Unlike validation, where an operator may want to be
|
|
1816
|
+
* told about them and decide, an invisible character reaching rendered HTML is
|
|
1817
|
+
* never what the author meant.
|
|
1818
|
+
*
|
|
1819
|
+
* The output is a fragment, not a document: no wrapper element, no doctype.
|
|
1820
|
+
* It is meant to be inserted into a page whose own Content-Security-Policy is
|
|
1821
|
+
* doing its job, not to replace one.
|
|
1822
|
+
*
|
|
1823
|
+
* What the profile changes is worth stating, because the two halves differ.
|
|
1824
|
+
* The SAFETY floor is profile-independent — escaping, the link-target
|
|
1825
|
+
* allowlist and raw-HTML-as-text are identical at every profile, since there
|
|
1826
|
+
* is no safe way to loosen them. What varies is the SIZE budget, enforced
|
|
1827
|
+
* before anything is parsed: `maxBytes` (1 MiB / 8 MiB / 64 MiB, measured in
|
|
1828
|
+
* BYTES so a non-ASCII document is not silently allowed several times the
|
|
1829
|
+
* stated size), `maxLines` (4,096 / 32,768 / 262,144) and
|
|
1830
|
+
* `maxBlockquoteDepth` (16 / 64 / 256). Choose the profile for the document
|
|
1831
|
+
* sizes you intend to accept, not for how much escaping you want.
|
|
1832
|
+
*
|
|
1833
|
+
* A `maxBlockquoteDepth` raised past 512 is bounded at 512, because the
|
|
1834
|
+
* renderer recurses once per level and what the call stack survives is not an
|
|
1835
|
+
* operator setting. Nesting beyond the effective bound is refused with
|
|
1836
|
+
* `markdown/blockquote-depth`, which is a verdict a caller can handle, rather
|
|
1837
|
+
* than the stack overflow it would otherwise become.
|
|
1838
|
+
*
|
|
1839
|
+
* @opts
|
|
1840
|
+
* profile: "strict"|"balanced"|"permissive",
|
|
1841
|
+
* compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
|
|
1842
|
+
* ...: same shape as b.guardMarkdown.validate opts,
|
|
1843
|
+
*
|
|
1844
|
+
* @example
|
|
1845
|
+
* b.guardMarkdown.render("# Title\n\nA [link](https://example.com).");
|
|
1846
|
+
* // -> "<h1>Title</h1>\n<p>A <a href=\"https://example.com\" rel=\"nofollow noopener noreferrer\">link</a>.</p>\n"
|
|
1847
|
+
*
|
|
1848
|
+
* b.guardMarkdown.render("[x](javascript:alert(1))");
|
|
1849
|
+
* // -> "<p>x</p>\n" (the target is refused, the text survives)
|
|
1850
|
+
*/
|
|
1851
|
+
function render(source, opts) {
|
|
1852
|
+
if (typeof source !== "string") {
|
|
1853
|
+
throw _err("markdown/bad-input",
|
|
1854
|
+
"b.guardMarkdown.render: source must be a string; got " + typeof source);
|
|
1855
|
+
}
|
|
1856
|
+
// resolveOpts refuses an unknown profile / posture, so a typo is a boot
|
|
1857
|
+
// error rather than a silent fall back to the loosest behaviour.
|
|
1858
|
+
var resolved = module.exports.resolveOpts(opts);
|
|
1859
|
+
|
|
1860
|
+
// The profile's input caps bind here exactly as they do in validate. Without
|
|
1861
|
+
// them the documented options would be decorative on this path and an
|
|
1862
|
+
// attacker could hand over an arbitrarily large document to strip and parse
|
|
1863
|
+
// — and this input is untrusted by definition, which is the whole reason the
|
|
1864
|
+
// caps exist.
|
|
1865
|
+
//
|
|
1866
|
+
// BYTES, via Buffer.byteLength, not `source.length`. The cap is named
|
|
1867
|
+
// maxBytes and a character count is up to 4x short of it on non-ASCII input,
|
|
1868
|
+
// so measuring the wrong representation would leave the cap silently wrong
|
|
1869
|
+
// for exactly the documents most likely to be hostile.
|
|
1870
|
+
var byteLen = Buffer.byteLength(source, "utf8");
|
|
1871
|
+
if (byteLen > resolved.maxBytes) {
|
|
1872
|
+
throw _err("markdown/too-large",
|
|
1873
|
+
"b.guardMarkdown.render: source is " + byteLen + " bytes, over the " +
|
|
1874
|
+
resolved.maxBytes + "-byte maxBytes for this profile");
|
|
1875
|
+
}
|
|
1876
|
+
var lineCount = _markdownLines(source).length;
|
|
1877
|
+
if (lineCount > resolved.maxLines) {
|
|
1878
|
+
throw _err("markdown/too-many-lines",
|
|
1879
|
+
"b.guardMarkdown.render: source has " + lineCount + " lines, over the " +
|
|
1880
|
+
resolved.maxLines + "-line maxLines for this profile");
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
var text = codepointClass.applyCharStripPolicies(source, {
|
|
1884
|
+
bidiPolicy: "strip",
|
|
1885
|
+
controlPolicy: "strip",
|
|
1886
|
+
nullBytePolicy: "strip",
|
|
1887
|
+
zeroWidthPolicy: "strip",
|
|
1888
|
+
});
|
|
1889
|
+
_outputBudget = {
|
|
1890
|
+
used: 0,
|
|
1891
|
+
max: byteLen < MIN_SOURCE_FOR_RATIO
|
|
1892
|
+
? Infinity
|
|
1893
|
+
: byteLen * MAX_OUTPUT_AMPLIFICATION,
|
|
1894
|
+
sourceBytes: byteLen,
|
|
1895
|
+
};
|
|
1896
|
+
try {
|
|
1897
|
+
return _renderBlocks(_markdownLines(text), resolved);
|
|
1898
|
+
} finally {
|
|
1899
|
+
// Cleared whether the render returned or a cap refused, so one call's
|
|
1900
|
+
// budget can never be inherited by the next.
|
|
1901
|
+
_outputBudget = null;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
|
|
967
1905
|
module.exports = gateContract.defineGuard({
|
|
968
1906
|
name: "markdown",
|
|
969
1907
|
kind: "content",
|
|
@@ -986,7 +1924,13 @@ module.exports = gateContract.defineGuard({
|
|
|
986
1924
|
"maxRefDefs", "maxListDepth", "maxBlockquoteDepth"],
|
|
987
1925
|
gate: gate,
|
|
988
1926
|
extra: {
|
|
1927
|
+
render: render,
|
|
989
1928
|
_gateDispositionForTest: _gateDispositionFor,
|
|
1929
|
+
_bracketMapsBuiltForTest: function () { return _bracketMapsBuilt; },
|
|
1930
|
+
_bracketArraysAllocatedForTest: function () { return _bracketArraysAllocated; },
|
|
1931
|
+
_bracketIndexEntriesForTest: function () { return _bracketIndexEntries; },
|
|
1932
|
+
_bracketLookupStepsForTest: function () { return _bracketLookupSteps; },
|
|
1933
|
+
_blockOffsetArraysForTest: function () { return _blockOffsetArrays; },
|
|
990
1934
|
// The extractors and shape screens, exposed so the test can compare each
|
|
991
1935
|
// against the pattern it replaced rather than only through a whole-document
|
|
992
1936
|
// scan.
|