aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2017-02-27 19:45:55 +0100
committerArthurHoaro <arthur@hoa.ro>2017-02-28 19:16:54 +0100
commite03761011521929a375ebb56f21adacb226a3a8d (patch)
tree6cc318939e74a35d74a037f18bca912b73e5c81e
parent5978588578ca103152598ccfbe41019b12e00a4f (diff)
downloadShaarli-e03761011521929a375ebb56f21adacb226a3a8d.tar.gz
Shaarli-e03761011521929a375ebb56f21adacb226a3a8d.tar.zst
Shaarli-e03761011521929a375ebb56f21adacb226a3a8d.zip
Add markdown_escape setting
This setting allows to escape HTML in markdown rendering or not. The goal behind it is to avoid XSS issue in shared instances. More info: * the setting is set to true by default * it is set to false for anyone who already have the plugin enabled (avoid breaking existing entries) * improve the HTML sanitization when the setting is set to false - but don't consider it XSS proof * mention the setting in the plugin README
-rw-r--r--application/Updater.php23
-rw-r--r--plugins/markdown/README.md27
-rw-r--r--plugins/markdown/markdown.php29
-rw-r--r--tests/Updater/UpdaterTest.php66
-rw-r--r--tests/plugins/PluginMarkdownTest.php57
-rw-r--r--tests/plugins/resources/markdown.html6
6 files changed, 181 insertions, 27 deletions
diff --git a/application/Updater.php b/application/Updater.php
index 3f5d325b..f5ebf31a 100644
--- a/application/Updater.php
+++ b/application/Updater.php
@@ -336,6 +336,29 @@ class Updater
336 } 336 }
337 $this->conf->set('resource.theme', 'vintage'); 337 $this->conf->set('resource.theme', 'vintage');
338 $this->conf->write($this->isLoggedIn); 338 $this->conf->write($this->isLoggedIn);
339
340 return true;
341 }
342
343 /**
344 * * `markdown_escape` is a new setting, set to true as default.
345 *
346 * If the markdown plugin was already enabled, escaping is disabled to avoid
347 * breaking existing entries.
348 */
349 public function updateMethodEscapeMarkdown()
350 {
351 if ($this->conf->exists('security.markdown_escape')) {
352 return true;
353 }
354
355 if (in_array('markdown', $this->conf->get('general.enabled_plugins'))) {
356 $this->conf->set('security.markdown_escape', false);
357 } else {
358 $this->conf->set('security.markdown_escape', true);
359 }
360 $this->conf->write($this->isLoggedIn);
361
339 return true; 362 return true;
340 } 363 }
341} 364}
diff --git a/plugins/markdown/README.md b/plugins/markdown/README.md
index aafcf066..bc9427e2 100644
--- a/plugins/markdown/README.md
+++ b/plugins/markdown/README.md
@@ -50,9 +50,20 @@ If the tag `nomarkdown` is set for a shaare, it won't be converted to Markdown s
50 50
51> Note: this is a special tag, so it won't be displayed in link list. 51> Note: this is a special tag, so it won't be displayed in link list.
52 52
53### HTML rendering 53### HTML escape
54 54
55Markdown support HTML tags. For example: 55By default, HTML tags are escaped. You can enable HTML tags rendering
56by setting `security.markdwon_escape` to `false` in `data/config.json.php`:
57
58```json
59{
60 "security": {
61 "markdown_escape": false
62 }
63}
64```
65
66With this setting, Markdown support HTML tags. For example:
56 67
57 > <strong>strong</strong><strike>strike</strike> 68 > <strong>strong</strong><strike>strike</strike>
58 69
@@ -60,12 +71,14 @@ Will render as:
60 71
61> <strong>strong</strong><strike>strike</strike> 72> <strong>strong</strong><strike>strike</strike>
62 73
63If you want to shaare HTML code, it is necessary to use inline code or code blocks.
64
65**If your shaared descriptions containing HTML tags before enabling the markdown plugin,
66enabling it might break your page.**
67 74
68> Note: HTML tags such as script, iframe, etc. are disabled for security reasons. 75**Warning:**
76
77 * This setting might present **security risks** (XSS) on shared instances, even though tags
78 such as script, iframe, etc should be disabled.
79 * If you want to shaare HTML code, it is necessary to use inline code or code blocks.
80 * If your shaared descriptions contained HTML tags before enabling the markdown plugin,
81enabling it might break your page.
69 82
70### Known issue 83### Known issue
71 84
diff --git a/plugins/markdown/markdown.php b/plugins/markdown/markdown.php
index 0cf6e6e2..de7c823d 100644
--- a/plugins/markdown/markdown.php
+++ b/plugins/markdown/markdown.php
@@ -14,18 +14,19 @@ define('NO_MD_TAG', 'nomarkdown');
14/** 14/**
15 * Parse linklist descriptions. 15 * Parse linklist descriptions.
16 * 16 *
17 * @param array $data linklist data. 17 * @param array $data linklist data.
18 * @param ConfigManager $conf instance.
18 * 19 *
19 * @return mixed linklist data parsed in markdown (and converted to HTML). 20 * @return mixed linklist data parsed in markdown (and converted to HTML).
20 */ 21 */
21function hook_markdown_render_linklist($data) 22function hook_markdown_render_linklist($data, $conf)
22{ 23{
23 foreach ($data['links'] as &$value) { 24 foreach ($data['links'] as &$value) {
24 if (!empty($value['tags']) && noMarkdownTag($value['tags'])) { 25 if (!empty($value['tags']) && noMarkdownTag($value['tags'])) {
25 $value = stripNoMarkdownTag($value); 26 $value = stripNoMarkdownTag($value);
26 continue; 27 continue;
27 } 28 }
28 $value['description'] = process_markdown($value['description']); 29 $value['description'] = process_markdown($value['description'], $conf->get('security.markdown_escape', true));
29 } 30 }
30 return $data; 31 return $data;
31} 32}
@@ -34,17 +35,18 @@ function hook_markdown_render_linklist($data)
34 * Parse feed linklist descriptions. 35 * Parse feed linklist descriptions.
35 * 36 *
36 * @param array $data linklist data. 37 * @param array $data linklist data.
38 * @param ConfigManager $conf instance.
37 * 39 *
38 * @return mixed linklist data parsed in markdown (and converted to HTML). 40 * @return mixed linklist data parsed in markdown (and converted to HTML).
39 */ 41 */
40function hook_markdown_render_feed($data) 42function hook_markdown_render_feed($data, $conf)
41{ 43{
42 foreach ($data['links'] as &$value) { 44 foreach ($data['links'] as &$value) {
43 if (!empty($value['tags']) && noMarkdownTag($value['tags'])) { 45 if (!empty($value['tags']) && noMarkdownTag($value['tags'])) {
44 $value = stripNoMarkdownTag($value); 46 $value = stripNoMarkdownTag($value);
45 continue; 47 continue;
46 } 48 }
47 $value['description'] = process_markdown($value['description']); 49 $value['description'] = process_markdown($value['description'], $conf->get('security.markdown_escape', true));
48 } 50 }
49 51
50 return $data; 52 return $data;
@@ -53,11 +55,12 @@ function hook_markdown_render_feed($data)
53/** 55/**
54 * Parse daily descriptions. 56 * Parse daily descriptions.
55 * 57 *
56 * @param array $data daily data. 58 * @param array $data daily data.
59 * @param ConfigManager $conf instance.
57 * 60 *
58 * @return mixed daily data parsed in markdown (and converted to HTML). 61 * @return mixed daily data parsed in markdown (and converted to HTML).
59 */ 62 */
60function hook_markdown_render_daily($data) 63function hook_markdown_render_daily($data, $conf)
61{ 64{
62 // Manipulate columns data 65 // Manipulate columns data
63 foreach ($data['cols'] as &$value) { 66 foreach ($data['cols'] as &$value) {
@@ -66,7 +69,10 @@ function hook_markdown_render_daily($data)
66 $value2 = stripNoMarkdownTag($value2); 69 $value2 = stripNoMarkdownTag($value2);
67 continue; 70 continue;
68 } 71 }
69 $value2['formatedDescription'] = process_markdown($value2['formatedDescription']); 72 $value2['formatedDescription'] = process_markdown(
73 $value2['formatedDescription'],
74 $conf->get('security.markdown_escape', true)
75 );
70 } 76 }
71 } 77 }
72 78
@@ -250,7 +256,7 @@ function sanitize_html($description)
250 $description); 256 $description);
251 } 257 }
252 $description = preg_replace( 258 $description = preg_replace(
253 '#(<[^>]+)on[a-z]*="[^"]*"#is', 259 '#(<[^>]+)on[a-z]*="?[^ "]*"?#is',
254 '$1', 260 '$1',
255 $description); 261 $description);
256 return $description; 262 return $description;
@@ -265,10 +271,11 @@ function sanitize_html($description)
265 * 5. Wrap description in 'markdown' CSS class. 271 * 5. Wrap description in 'markdown' CSS class.
266 * 272 *
267 * @param string $description input description text. 273 * @param string $description input description text.
274 * @param bool $escape escape HTML entities
268 * 275 *
269 * @return string HTML processed $description. 276 * @return string HTML processed $description.
270 */ 277 */
271function process_markdown($description) 278function process_markdown($description, $escape = true)
272{ 279{
273 $parsedown = new Parsedown(); 280 $parsedown = new Parsedown();
274 281
@@ -278,7 +285,7 @@ function process_markdown($description)
278 $processedDescription = reverse_text2clickable($processedDescription); 285 $processedDescription = reverse_text2clickable($processedDescription);
279 $processedDescription = unescape($processedDescription); 286 $processedDescription = unescape($processedDescription);
280 $processedDescription = $parsedown 287 $processedDescription = $parsedown
281 ->setMarkupEscaped(false) 288 ->setMarkupEscaped($escape)
282 ->setBreaksEnabled(true) 289 ->setBreaksEnabled(true)
283 ->text($processedDescription); 290 ->text($processedDescription);
284 $processedDescription = sanitize_html($processedDescription); 291 $processedDescription = sanitize_html($processedDescription);
diff --git a/tests/Updater/UpdaterTest.php b/tests/Updater/UpdaterTest.php
index de330ae2..39be88f9 100644
--- a/tests/Updater/UpdaterTest.php
+++ b/tests/Updater/UpdaterTest.php
@@ -506,4 +506,70 @@ $GLOBALS[\'privateLinkByDefault\'] = true;';
506 $this->conf = new ConfigManager($sandboxConf); 506 $this->conf = new ConfigManager($sandboxConf);
507 $this->assertEquals($theme, $this->conf->get('resource.theme')); 507 $this->assertEquals($theme, $this->conf->get('resource.theme'));
508 } 508 }
509
510 /**
511 * Test updateMethodEscapeMarkdown with markdown plugin enabled
512 * => setting markdown_escape set to false.
513 */
514 public function testEscapeMarkdownSettingToFalse()
515 {
516 $sandboxConf = 'sandbox/config';
517 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
518 $this->conf = new ConfigManager($sandboxConf);
519
520 $this->conf->set('general.enabled_plugins', ['markdown']);
521 $updater = new Updater([], [], $this->conf, true);
522 $this->assertTrue($updater->updateMethodEscapeMarkdown());
523 $this->assertFalse($this->conf->get('security.markdown_escape'));
524
525 // reload from file
526 $this->conf = new ConfigManager($sandboxConf);
527 $this->assertFalse($this->conf->get('security.markdown_escape'));
528 }
529
530
531 /**
532 * Test updateMethodEscapeMarkdown with markdown plugin disabled
533 * => setting markdown_escape set to true.
534 */
535 public function testEscapeMarkdownSettingToTrue()
536 {
537 $sandboxConf = 'sandbox/config';
538 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
539 $this->conf = new ConfigManager($sandboxConf);
540
541 $this->conf->set('general.enabled_plugins', []);
542 $updater = new Updater([], [], $this->conf, true);
543 $this->assertTrue($updater->updateMethodEscapeMarkdown());
544 $this->assertTrue($this->conf->get('security.markdown_escape'));
545
546 // reload from file
547 $this->conf = new ConfigManager($sandboxConf);
548 $this->assertTrue($this->conf->get('security.markdown_escape'));
549 }
550
551 /**
552 * Test updateMethodEscapeMarkdown with nothing to do (setting already enabled)
553 */
554 public function testEscapeMarkdownSettingNothingToDoEnabled()
555 {
556 $sandboxConf = 'sandbox/config';
557 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
558 $this->conf = new ConfigManager($sandboxConf);
559 $this->conf->set('security.markdown_escape', true);
560 $updater = new Updater([], [], $this->conf, true);
561 $this->assertTrue($updater->updateMethodEscapeMarkdown());
562 $this->assertTrue($this->conf->get('security.markdown_escape'));
563 }
564
565 /**
566 * Test updateMethodEscapeMarkdown with nothing to do (setting already disabled)
567 */
568 public function testEscapeMarkdownSettingNothingToDoDisabled()
569 {
570 $this->conf->set('security.markdown_escape', false);
571 $updater = new Updater([], [], $this->conf, true);
572 $this->assertTrue($updater->updateMethodEscapeMarkdown());
573 $this->assertFalse($this->conf->get('security.markdown_escape'));
574 }
509} 575}
diff --git a/tests/plugins/PluginMarkdownTest.php b/tests/plugins/PluginMarkdownTest.php
index d359b2a1..d4cd1b97 100644
--- a/tests/plugins/PluginMarkdownTest.php
+++ b/tests/plugins/PluginMarkdownTest.php
@@ -14,11 +14,17 @@ require_once 'plugins/markdown/markdown.php';
14class PluginMarkdownTest extends PHPUnit_Framework_TestCase 14class PluginMarkdownTest extends PHPUnit_Framework_TestCase
15{ 15{
16 /** 16 /**
17 * @var ConfigManager instance.
18 */
19 protected $conf;
20
21 /**
17 * Reset plugin path 22 * Reset plugin path
18 */ 23 */
19 public function setUp() 24 public function setUp()
20 { 25 {
21 PluginManager::$PLUGINS_PATH = 'plugins'; 26 PluginManager::$PLUGINS_PATH = 'plugins';
27 $this->conf = new ConfigManager('tests/utils/config/configJson');
22 } 28 }
23 29
24 /** 30 /**
@@ -36,7 +42,7 @@ class PluginMarkdownTest extends PHPUnit_Framework_TestCase
36 ), 42 ),
37 ); 43 );
38 44
39 $data = hook_markdown_render_linklist($data); 45 $data = hook_markdown_render_linklist($data, $this->conf);
40 $this->assertNotFalse(strpos($data['links'][0]['description'], '<h1>')); 46 $this->assertNotFalse(strpos($data['links'][0]['description'], '<h1>'));
41 $this->assertNotFalse(strpos($data['links'][0]['description'], '<p>')); 47 $this->assertNotFalse(strpos($data['links'][0]['description'], '<p>'));
42 } 48 }
@@ -61,7 +67,7 @@ class PluginMarkdownTest extends PHPUnit_Framework_TestCase
61 ), 67 ),
62 ); 68 );
63 69
64 $data = hook_markdown_render_daily($data); 70 $data = hook_markdown_render_daily($data, $this->conf);
65 $this->assertNotFalse(strpos($data['cols'][0][0]['formatedDescription'], '<h1>')); 71 $this->assertNotFalse(strpos($data['cols'][0][0]['formatedDescription'], '<h1>'));
66 $this->assertNotFalse(strpos($data['cols'][0][0]['formatedDescription'], '<p>')); 72 $this->assertNotFalse(strpos($data['cols'][0][0]['formatedDescription'], '<p>'));
67 } 73 }
@@ -110,6 +116,8 @@ class PluginMarkdownTest extends PHPUnit_Framework_TestCase
110 $output = escape($input); 116 $output = escape($input);
111 $input .= '<a href="#" onmouseHover="alert(\'xss\');" attr="tt">link</a>'; 117 $input .= '<a href="#" onmouseHover="alert(\'xss\');" attr="tt">link</a>';
112 $output .= '<a href="#" attr="tt">link</a>'; 118 $output .= '<a href="#" attr="tt">link</a>';
119 $input .= '<a href="#" onmouseHover=alert(\'xss\'); attr="tt">link</a>';
120 $output .= '<a href="#" attr="tt">link</a>';
113 $this->assertEquals($output, sanitize_html($input)); 121 $this->assertEquals($output, sanitize_html($input));
114 // Do not touch escaped HTML. 122 // Do not touch escaped HTML.
115 $input = escape($input); 123 $input = escape($input);
@@ -130,10 +138,10 @@ class PluginMarkdownTest extends PHPUnit_Framework_TestCase
130 )) 138 ))
131 ); 139 );
132 140
133 $processed = hook_markdown_render_linklist($data); 141 $processed = hook_markdown_render_linklist($data, $this->conf);
134 $this->assertEquals($str, $processed['links'][0]['description']); 142 $this->assertEquals($str, $processed['links'][0]['description']);
135 143
136 $processed = hook_markdown_render_feed($data); 144 $processed = hook_markdown_render_feed($data, $this->conf);
137 $this->assertEquals($str, $processed['links'][0]['description']); 145 $this->assertEquals($str, $processed['links'][0]['description']);
138 146
139 $data = array( 147 $data = array(
@@ -151,7 +159,7 @@ class PluginMarkdownTest extends PHPUnit_Framework_TestCase
151 ), 159 ),
152 ); 160 );
153 161
154 $data = hook_markdown_render_daily($data); 162 $data = hook_markdown_render_daily($data, $this->conf);
155 $this->assertEquals($str, $data['cols'][0][0]['formatedDescription']); 163 $this->assertEquals($str, $data['cols'][0][0]['formatedDescription']);
156 } 164 }
157 165
@@ -169,7 +177,7 @@ class PluginMarkdownTest extends PHPUnit_Framework_TestCase
169 )) 177 ))
170 ); 178 );
171 179
172 $data = hook_markdown_render_feed($data); 180 $data = hook_markdown_render_feed($data, $this->conf);
173 $this->assertContains('<em>', $data['links'][0]['description']); 181 $this->assertContains('<em>', $data['links'][0]['description']);
174 } 182 }
175 183
@@ -185,4 +193,41 @@ class PluginMarkdownTest extends PHPUnit_Framework_TestCase
185 $data = process_markdown($md); 193 $data = process_markdown($md);
186 $this->assertEquals($html, $data); 194 $this->assertEquals($html, $data);
187 } 195 }
196
197 /**
198 * Make sure that the HTML tags are escaped.
199 */
200 public function testMarkdownWithHtmlEscape()
201 {
202 $md = '**strong** <strong>strong</strong>';
203 $html = '<div class="markdown"><p><strong>strong</strong> &lt;strong&gt;strong&lt;/strong&gt;</p></div>';
204 $data = array(
205 'links' => array(
206 0 => array(
207 'description' => $md,
208 ),
209 ),
210 );
211 $data = hook_markdown_render_linklist($data, $this->conf);
212 $this->assertEquals($html, $data['links'][0]['description']);
213 }
214
215 /**
216 * Make sure that the HTML tags aren't escaped with the setting set to false.
217 */
218 public function testMarkdownWithHtmlNoEscape()
219 {
220 $this->conf->set('security.markdown_escape', false);
221 $md = '**strong** <strong>strong</strong>';
222 $html = '<div class="markdown"><p><strong>strong</strong> <strong>strong</strong></p></div>';
223 $data = array(
224 'links' => array(
225 0 => array(
226 'description' => $md,
227 ),
228 ),
229 );
230 $data = hook_markdown_render_linklist($data, $this->conf);
231 $this->assertEquals($html, $data['links'][0]['description']);
232 }
188} 233}
diff --git a/tests/plugins/resources/markdown.html b/tests/plugins/resources/markdown.html
index c0fbe7f4..07a5a32e 100644
--- a/tests/plugins/resources/markdown.html
+++ b/tests/plugins/resources/markdown.html
@@ -12,11 +12,11 @@
12<li><a href="http://link.tld">two</a></li> 12<li><a href="http://link.tld">two</a></li>
13<li><a href="http://link.tld">three</a></li> 13<li><a href="http://link.tld">three</a></li>
14<li><a href="http://link.tld">four</a></li> 14<li><a href="http://link.tld">four</a></li>
15<li>foo <a href="?addtag=foobar" title="Hashtag foobar">#foobar</a></li> 15<li>foo &lt;a href=&quot;?addtag=foobar&quot; title=&quot;Hashtag foobar&quot;&gt;#foobar&lt;/a&gt;</li>
16</ol></li> 16</ol></li>
17</ol> 17</ol>
18<p><a href="?addtag=foobar" title="Hashtag foobar">#foobar</a> foo <code>lol #foo</code> <a href="?addtag=bar" title="Hashtag bar">#bar</a></p> 18<p>&lt;a href=&quot;?addtag=foobar&quot; title=&quot;Hashtag foobar&quot;&gt;#foobar&lt;/a&gt; foo <code>lol #foo</code> &lt;a href=&quot;?addtag=bar&quot; title=&quot;Hashtag bar&quot;&gt;#bar&lt;/a&gt;</p>
19<p>fsdfs <a href="http://link.tld">http://link.tld</a> <a href="?addtag=foobar" title="Hashtag foobar">#foobar</a> <code>http://link.tld</code></p> 19<p>fsdfs <a href="http://link.tld">http://link.tld</a> &lt;a href=&quot;?addtag=foobar&quot; title=&quot;Hashtag foobar&quot;&gt;#foobar&lt;/a&gt; <code>http://link.tld</code></p>
20<pre><code>http://link.tld #foobar 20<pre><code>http://link.tld #foobar
21next #foo</code></pre> 21next #foo</code></pre>
22<p>Block:</p> 22<p>Block:</p>