serialize-javascript.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. "use strict";
  2. // @ts-nocheck
  3. var g = typeof globalThis !== 'undefined' ? globalThis : global;
  4. var crypto = g.crypto || {};
  5. if (typeof crypto.getRandomValues !== 'function') {
  6. var nodeCrypto = require('crypto');
  7. crypto.getRandomValues = function (typedArray) {
  8. var bytes = nodeCrypto.randomBytes(typedArray.byteLength);
  9. new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength).set(bytes);
  10. return typedArray;
  11. };
  12. }
  13. /*
  14. Copyright (c) 2014, Yahoo! Inc. All rights reserved.
  15. Copyrights licensed under the New BSD License.
  16. See the accompanying LICENSE file for terms.
  17. */
  18. 'use strict';
  19. // Generate an internal UID to make the regexp pattern harder to guess.
  20. var UID_LENGTH = 16;
  21. var UID = generateUID();
  22. var PLACE_HOLDER_REGEXP = new RegExp('(\\\\)?"@__(F|R|D|M|S|A|U|I|B|L)-' + UID + '-(\\d+)__@"', 'g');
  23. var IS_NATIVE_CODE_REGEXP = /\{\s*\[native code\]\s*\}/g;
  24. var IS_PURE_FUNCTION = /function.*?\(/;
  25. var IS_ARROW_FUNCTION = /.*?=>.*?/;
  26. var UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g;
  27. // Regex to match </script> and variations (case-insensitive) for XSS protection
  28. // Matches </script followed by optional whitespace/attributes and >
  29. var SCRIPT_CLOSE_REGEXP = /<\/script[^>]*>/gi;
  30. var RESERVED_SYMBOLS = ['*', 'async'];
  31. // Mapping of unsafe HTML and invalid JavaScript line terminator chars to their
  32. // Unicode char counterparts which are safe to use in JavaScript strings.
  33. var ESCAPED_CHARS = {
  34. '<': '\\u003C',
  35. '>': '\\u003E',
  36. '/': '\\u002F',
  37. '\u2028': '\\u2028',
  38. '\u2029': '\\u2029'
  39. };
  40. function escapeUnsafeChars(unsafeChar) {
  41. return ESCAPED_CHARS[unsafeChar];
  42. }
  43. // Escape function body for XSS protection while preserving arrow function syntax
  44. function escapeFunctionBody(str) {
  45. // Escape </script> sequences and variations (case-insensitive) - the main XSS risk
  46. // Matches </script followed by optional whitespace/attributes and >
  47. // This must be done first before other replacements
  48. str = str.replace(SCRIPT_CLOSE_REGEXP, function (match) {
  49. // Escape all <, /, and > characters in the closing script tag
  50. return match.replace(/</g, '\\u003C').replace(/\//g, '\\u002F').replace(/>/g, '\\u003E');
  51. });
  52. // Escape line terminators (these are always unsafe)
  53. str = str.replace(/\u2028/g, '\\u2028');
  54. str = str.replace(/\u2029/g, '\\u2029');
  55. return str;
  56. }
  57. function generateUID() {
  58. var bytes = crypto.getRandomValues(new Uint8Array(UID_LENGTH));
  59. var result = '';
  60. for (var i = 0; i < UID_LENGTH; ++i) {
  61. result += bytes[i].toString(16);
  62. }
  63. return result;
  64. }
  65. function deleteFunctions(obj) {
  66. var functionKeys = [];
  67. for (var key in obj) {
  68. if (typeof obj[key] === "function") {
  69. functionKeys.push(key);
  70. }
  71. }
  72. for (var i = 0; i < functionKeys.length; i++) {
  73. delete obj[functionKeys[i]];
  74. }
  75. }
  76. module.exports = function serialize(obj, options) {
  77. options || (options = {});
  78. // Backwards-compatibility for `space` as the second argument.
  79. if (typeof options === 'number' || typeof options === 'string') {
  80. options = {
  81. space: options
  82. };
  83. }
  84. var functions = [];
  85. var regexps = [];
  86. var dates = [];
  87. var maps = [];
  88. var sets = [];
  89. var arrays = [];
  90. var undefs = [];
  91. var infinities = [];
  92. var bigInts = [];
  93. var urls = [];
  94. // Returns placeholders for functions and regexps (identified by index)
  95. // which are later replaced by their string representation.
  96. function replacer(key, value) {
  97. // For nested function
  98. if (options.ignoreFunction) {
  99. deleteFunctions(value);
  100. }
  101. if (!value && value !== undefined && value !== BigInt(0)) {
  102. return value;
  103. }
  104. // If the value is an object w/ a toJSON method, toJSON is called before
  105. // the replacer runs, so we use this[key] to get the non-toJSONed value.
  106. var origValue = this[key];
  107. var type = typeof origValue;
  108. if (type === 'object') {
  109. if (origValue instanceof RegExp) {
  110. return '@__R-' + UID + '-' + (regexps.push(origValue) - 1) + '__@';
  111. }
  112. if (origValue instanceof Date) {
  113. return '@__D-' + UID + '-' + (dates.push(origValue) - 1) + '__@';
  114. }
  115. if (origValue instanceof Map) {
  116. return '@__M-' + UID + '-' + (maps.push(origValue) - 1) + '__@';
  117. }
  118. if (origValue instanceof Set) {
  119. return '@__S-' + UID + '-' + (sets.push(origValue) - 1) + '__@';
  120. }
  121. if (origValue instanceof Array) {
  122. var isSparse = origValue.filter(function () {
  123. return true;
  124. }).length !== origValue.length;
  125. if (isSparse) {
  126. return '@__A-' + UID + '-' + (arrays.push(origValue) - 1) + '__@';
  127. }
  128. }
  129. if (origValue instanceof URL) {
  130. return '@__L-' + UID + '-' + (urls.push(origValue) - 1) + '__@';
  131. }
  132. }
  133. if (type === 'function') {
  134. return '@__F-' + UID + '-' + (functions.push(origValue) - 1) + '__@';
  135. }
  136. if (type === 'undefined') {
  137. return '@__U-' + UID + '-' + (undefs.push(origValue) - 1) + '__@';
  138. }
  139. if (type === 'number' && !isNaN(origValue) && !isFinite(origValue)) {
  140. return '@__I-' + UID + '-' + (infinities.push(origValue) - 1) + '__@';
  141. }
  142. if (type === 'bigint') {
  143. return '@__B-' + UID + '-' + (bigInts.push(origValue) - 1) + '__@';
  144. }
  145. return value;
  146. }
  147. function serializeFunc(fn, options) {
  148. var serializedFn = fn.toString();
  149. if (IS_NATIVE_CODE_REGEXP.test(serializedFn)) {
  150. throw new TypeError('Serializing native function: ' + fn.name);
  151. }
  152. // Escape unsafe HTML characters in function body for XSS protection
  153. // This must preserve arrow function syntax (=>) while escaping </script>
  154. if (options && options.unsafe !== true) {
  155. serializedFn = escapeFunctionBody(serializedFn);
  156. }
  157. // pure functions, example: {key: function() {}}
  158. if (IS_PURE_FUNCTION.test(serializedFn)) {
  159. return serializedFn;
  160. }
  161. // arrow functions, example: arg1 => arg1+5
  162. if (IS_ARROW_FUNCTION.test(serializedFn)) {
  163. return serializedFn;
  164. }
  165. var argsStartsAt = serializedFn.indexOf('(');
  166. var def = serializedFn.substr(0, argsStartsAt).trim().split(' ').filter(function (val) {
  167. return val.length > 0;
  168. });
  169. var nonReservedSymbols = def.filter(function (val) {
  170. return RESERVED_SYMBOLS.indexOf(val) === -1;
  171. });
  172. // enhanced literal objects, example: {key() {}}
  173. if (nonReservedSymbols.length > 0) {
  174. return (def.indexOf('async') > -1 ? 'async ' : '') + 'function' + (def.join('').indexOf('*') > -1 ? '*' : '') + serializedFn.substr(argsStartsAt);
  175. }
  176. // arrow functions
  177. return serializedFn;
  178. }
  179. // Check if the parameter is function
  180. if (options.ignoreFunction && typeof obj === "function") {
  181. obj = undefined;
  182. }
  183. // Protects against `JSON.stringify()` returning `undefined`, by serializing
  184. // to the literal string: "undefined".
  185. if (obj === undefined) {
  186. return String(obj);
  187. }
  188. var str;
  189. // Creates a JSON string representation of the value.
  190. // NOTE: Node 0.12 goes into slow mode with extra JSON.stringify() args.
  191. if (options.isJSON && !options.space) {
  192. str = JSON.stringify(obj);
  193. } else {
  194. str = JSON.stringify(obj, options.isJSON ? null : replacer, options.space);
  195. }
  196. // Protects against `JSON.stringify()` returning `undefined`, by serializing
  197. // to the literal string: "undefined".
  198. if (typeof str !== 'string') {
  199. return String(str);
  200. }
  201. // Replace unsafe HTML and invalid JavaScript line terminator chars with
  202. // their safe Unicode char counterpart. This _must_ happen before the
  203. // regexps and functions are serialized and added back to the string.
  204. if (options.unsafe !== true) {
  205. str = str.replace(UNSAFE_CHARS_REGEXP, escapeUnsafeChars);
  206. }
  207. if (functions.length === 0 && regexps.length === 0 && dates.length === 0 && maps.length === 0 && sets.length === 0 && arrays.length === 0 && undefs.length === 0 && infinities.length === 0 && bigInts.length === 0 && urls.length === 0) {
  208. return str;
  209. }
  210. // Replaces all occurrences of function, regexp, date, map and set placeholders in the
  211. // JSON string with their string representations. If the original value can
  212. // not be found, then `undefined` is used.
  213. return str.replace(PLACE_HOLDER_REGEXP, function (match, backSlash, type, valueIndex) {
  214. // The placeholder may not be preceded by a backslash. This is to prevent
  215. // replacing things like `"a\"@__R-<UID>-0__@"` and thus outputting
  216. // invalid JS.
  217. if (backSlash) {
  218. return match;
  219. }
  220. if (type === 'D') {
  221. // Validate ISO string format to prevent code injection via spoofed toISOString()
  222. var isoStr = String(dates[valueIndex].toISOString());
  223. if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(isoStr)) {
  224. throw new TypeError('Invalid Date ISO string');
  225. }
  226. return "new Date(\"" + isoStr + "\")";
  227. }
  228. if (type === 'R') {
  229. // Sanitize flags to prevent code injection (only allow valid RegExp flag characters)
  230. var flags = String(regexps[valueIndex].flags).replace(/[^gimsuydv]/g, '');
  231. return "new RegExp(" + serialize(regexps[valueIndex].source) + ", \"" + flags + "\")";
  232. }
  233. if (type === 'M') {
  234. return "new Map(" + serialize(Array.from(maps[valueIndex].entries()), options) + ")";
  235. }
  236. if (type === 'S') {
  237. return "new Set(" + serialize(Array.from(sets[valueIndex].values()), options) + ")";
  238. }
  239. if (type === 'A') {
  240. return "Array.prototype.slice.call(" + serialize(Object.assign({
  241. length: arrays[valueIndex].length
  242. }, arrays[valueIndex]), options) + ")";
  243. }
  244. if (type === 'U') {
  245. return 'undefined';
  246. }
  247. if (type === 'I') {
  248. return infinities[valueIndex];
  249. }
  250. if (type === 'B') {
  251. return "BigInt(\"" + bigInts[valueIndex] + "\")";
  252. }
  253. if (type === 'L') {
  254. return "new URL(" + serialize(urls[valueIndex].toString(), options) + ")";
  255. }
  256. var fn = functions[valueIndex];
  257. return serializeFunc(fn, options);
  258. });
  259. };