template-integration.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import DOMPurify from 'dompurify';
  2. import { getSecureDOMPurifyConfig } from './secureDOMPurify';
  3. var Markdown = require('markdown-it')({
  4. html: true,
  5. linkify: true,
  6. typographer: true,
  7. breaks: true,
  8. });
  9. //import markdownItMermaid from "@wekanteam/markdown-it-mermaid";
  10. // Static URL Scheme Listing
  11. var urlschemes = [
  12. "aodroplink",
  13. "thunderlink",
  14. "cbthunderlink",
  15. "onenote",
  16. "file",
  17. "abasurl",
  18. "conisio",
  19. "mailspring"
  20. ];
  21. // Better would be a field in the admin backend to set this dynamically
  22. // instead of putting all known or wanted url schemes here hard into code
  23. // but i was not able to access those settings
  24. // var urlschemes = currentSetting.automaticLinkedUrlSchemes.split('\n');
  25. // put all url schemes into the linkify configuration to automatically make it clickable
  26. for(var i=0; i<urlschemes.length;i++){
  27. Markdown.linkify.add(urlschemes[i]+":",'http:');
  28. }
  29. // Try to load emoji support, but don't fail if it's not available
  30. try {
  31. var emoji = require('markdown-it-emoji');
  32. Markdown.use(emoji);
  33. } catch (e) {
  34. console.warn('markdown-it-emoji not available, emoji rendering disabled:', e.message);
  35. }
  36. // Try to load mathjax3, but don't fail if it's not available
  37. try {
  38. var mathjax = require('markdown-it-mathjax3');
  39. Markdown.use(mathjax);
  40. } catch (e) {
  41. console.warn('markdown-it-mathjax3 not available, math rendering disabled:', e.message);
  42. }
  43. // Custom plugin to prevent SVG-based DoS attacks
  44. Markdown.use(function(md) {
  45. // Filter out dangerous SVG content in markdown
  46. md.core.ruler.push('svg-dos-protection', function(state) {
  47. const tokens = state.tokens;
  48. for (let i = 0; i < tokens.length; i++) {
  49. const token = tokens[i];
  50. // Check for image tokens that might contain SVG
  51. if (token.type === 'image') {
  52. const src = token.attrGet('src');
  53. if (src) {
  54. // Block SVG data URIs and .svg files
  55. if (src.startsWith('data:image/svg') || src.endsWith('.svg')) {
  56. if (process.env.DEBUG === 'true') {
  57. console.warn('Blocked potentially malicious SVG image in markdown:', src);
  58. }
  59. // Replace with a warning message
  60. token.type = 'paragraph_open';
  61. token.tag = 'p';
  62. token.nesting = 1;
  63. token.attrSet('style', 'color: red; background: #ffe6e6; padding: 8px; border: 1px solid #ff9999;');
  64. token.attrSet('title', 'Blocked potentially malicious SVG image');
  65. // Add warning text token
  66. const warningToken = {
  67. type: 'text',
  68. content: '⚠️ Blocked potentially malicious SVG image for security reasons',
  69. level: token.level,
  70. markup: '',
  71. info: '',
  72. meta: null,
  73. block: true,
  74. hidden: false
  75. };
  76. // Insert warning token after the paragraph open
  77. tokens.splice(i + 1, 0, warningToken);
  78. // Add paragraph close token
  79. const closeToken = {
  80. type: 'paragraph_close',
  81. tag: 'p',
  82. nesting: -1,
  83. level: token.level,
  84. markup: '',
  85. info: '',
  86. meta: null,
  87. block: true,
  88. hidden: false
  89. };
  90. tokens.splice(i + 2, 0, closeToken);
  91. // Remove the original image token
  92. tokens.splice(i, 1);
  93. i--; // Adjust index since we removed a token
  94. }
  95. }
  96. }
  97. // Check for HTML tokens that might contain SVG or malicious content
  98. if (token.type === 'html_block' || token.type === 'html_inline') {
  99. const content = token.content;
  100. if (content) {
  101. // Check for SVG content
  102. const hasSVG = content.includes('<svg') ||
  103. content.includes('data:image/svg') ||
  104. content.includes('xlink:href') ||
  105. content.includes('<use') ||
  106. content.includes('<defs>');
  107. // Check for malicious img tags with SVG data URIs
  108. const hasMaliciousImg = content.includes('<img') &&
  109. (content.includes('data:image/svg') ||
  110. content.includes('src="data:image/svg'));
  111. // Check for base64 encoded SVG with script tags
  112. const hasBase64SVG = content.includes('data:image/svg+xml;base64,');
  113. if (hasSVG || hasMaliciousImg || hasBase64SVG) {
  114. if (process.env.DEBUG === 'true') {
  115. console.warn('Blocked potentially malicious SVG content in HTML:', content.substring(0, 100) + '...');
  116. }
  117. // Additional check for base64 encoded SVG with script tags
  118. if (hasBase64SVG) {
  119. try {
  120. const base64Match = content.match(/data:image\/svg\+xml;base64,([^"'\s]+)/);
  121. if (base64Match) {
  122. const decodedContent = atob(base64Match[1]);
  123. if (decodedContent.includes('<script') || decodedContent.includes('javascript:')) {
  124. if (process.env.DEBUG === 'true') {
  125. console.warn('Blocked SVG with embedded JavaScript in markdown');
  126. }
  127. }
  128. }
  129. } catch (e) {
  130. // If decoding fails, continue with blocking
  131. }
  132. }
  133. // Replace with warning
  134. token.type = 'paragraph_open';
  135. token.tag = 'p';
  136. token.nesting = 1;
  137. token.attrSet('style', 'color: red; background: #ffe6e6; padding: 8px; border: 1px solid #ff9999;');
  138. token.attrSet('title', 'Blocked potentially malicious SVG content');
  139. // Add warning text
  140. const warningToken = {
  141. type: 'text',
  142. content: '⚠️ Blocked potentially malicious SVG content for security reasons',
  143. level: token.level,
  144. markup: '',
  145. info: '',
  146. meta: null,
  147. block: true,
  148. hidden: false
  149. };
  150. // Insert warning token after the paragraph open
  151. tokens.splice(i + 1, 0, warningToken);
  152. // Add paragraph close token
  153. const closeToken = {
  154. type: 'paragraph_close',
  155. tag: 'p',
  156. nesting: -1,
  157. level: token.level,
  158. markup: '',
  159. info: '',
  160. meta: null,
  161. block: true,
  162. hidden: false
  163. };
  164. tokens.splice(i + 2, 0, closeToken);
  165. // Remove the original HTML token
  166. tokens.splice(i, 1);
  167. i--; // Adjust index since we removed a token
  168. }
  169. }
  170. }
  171. }
  172. });
  173. });
  174. // Try to fix Mermaid Diagram error: Maximum call stack size exceeded.
  175. // Added bigger text size for Diagram.
  176. // https://github.com/wekan/wekan/issues/4251
  177. // https://stackoverflow.com/questions/66825888/maximum-text-size-in-diagram-exceeded-mermaid-js
  178. // https://github.com/mermaid-js/mermaid/blob/74b1219d62dd76d98d60abeeb36d4520f64faceb/src/defaultConfig.js#L39
  179. // https://github.com/wekan/cli-table3
  180. // https://www.npmjs.com/package/@wekanteam/markdown-it-mermaid
  181. // https://github.com/wekan/markdown-it-mermaid
  182. //Markdown.use(markdownItMermaid,{
  183. // maxTextSize: 200000,
  184. //});
  185. if (Package.ui) {
  186. const Template = Package.templating.Template;
  187. const UI = Package.ui.UI;
  188. const HTML = Package.htmljs.HTML;
  189. const Blaze = Package.blaze.Blaze; // implied by `ui`
  190. UI.registerHelper('markdown', new Template('markdown', function () {
  191. const self = this;
  192. let text = '';
  193. if (self.templateContentBlock) {
  194. text = Blaze._toText(self.templateContentBlock, HTML.TEXTMODE.STRING);
  195. }
  196. if (text.includes("[]")) {
  197. // Prevent hiding info: https://wekan.github.io/hall-of-fame/invisiblebleed/
  198. // If markdown link does not have description, do not render markdown, instead show all of markdown source code using preformatted text.
  199. // Also show html comments.
  200. return HTML.Raw('<pre style="background-color: red;" title="Warning! Hidden markdown link description!" aria-label="Warning! Hidden markdown link description!">' + DOMPurify.sanitize(text.replace('<!--', '&lt;!--').replace('-->', '--&gt;'), getSecureDOMPurifyConfig()) + '</pre>');
  201. } else {
  202. // Prevent hiding info: https://wekan.github.io/hall-of-fame/invisiblebleed/
  203. // If text does not have hidden markdown link, render all markdown.
  204. // Also show html comments.
  205. const renderedMarkdown = Markdown.render(text).replace('<!--', '<font color="red" title="Warning! Hidden HTML comment!" aria-label="Warning! Hidden HTML comment!">&lt;!--</font>').replace('-->', '<font color="red" title="Warning! Hidden HTML comment!" aria-label="Warning! Hidden HTML comment!">--&gt;</font>');
  206. const sanitized = DOMPurify.sanitize(renderedMarkdown, getSecureDOMPurifyConfig());
  207. return HTML.Raw(sanitized);
  208. }
  209. }));
  210. }