Skip to content

Commit 1981e1e

Browse files
committed
Modified patch originally from Alexandre Erwin Ittner <[email protected]>
Liferea feed auto-discovery feature can create subscriptions to command feeds, allowing malicious websites to run arbitrary commands on the local system. For example, trying feed discovery in a site which returns a document like this: <!DOCTYPE html> <html> <head> <title>Feed auto-discovery RCE PoC</title> <link rel="alternate" type="application/rss+xml" href="|date &gt;/tmp/bad-feed-discovery.txt"> </head> <body> Oooops. </body> </html> will cause Liferea to subscribe to command "date >/tmp/bad-feed-discovery.txt". The command runs before the user has a chance of checking the subscription. This happens because function common_build_url, used to normalize the discovered URL candidates, fails to create an absolute URL if any of the parts has some special characters (including "|"), returning the original, non-escaped, relative URL instead. - Filter out unwanted link strings - Add testcase for HTML5 autodiscovery - Add testcase for item links
1 parent 8d8b5b9 commit 1981e1e

14 files changed

+178
-24
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ src/Liferea-3.0.gir
5050
src/Liferea-3.0.typelib
5151
src/tests/favicon
5252
src/tests/html_auto
53-
src/tests/parse_html
5453
src/tests/parse_date
54+
src/tests/parse_html
55+
src/tests/parse_rss
5556
src/tests/parse_xml
5657
src/tests/social
5758
xslt/*.xml

src/common.c

+3-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,9 @@ common_uri_escape (const xmlChar *url)
138138
g_assert (NULL != url);
139139

140140
/* xmlURIEscape returns NULL if spaces are in the URL,
141-
so we need to replace them first (see SF #2965158) */
141+
so we need to replace them first (see SF #2965158).
142+
TODO: perhaps replace xmlURIEscape with g_uri_escape_string ?
143+
*/
142144
tmp = (xmlChar *)common_strreplace (g_strdup ((gchar *)url), " ", "%20");
143145
result = xmlURIEscape (tmp);
144146
g_free (tmp);

src/feed.c

+1-2
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ feed_get_node_type (void)
460460
NODE_CAPABILITY_EXPORT |
461461
NODE_CAPABILITY_EXPORT_ITEMS,
462462
"feed", /* not used, feed format ids are used instead */
463-
NULL,
463+
ICON_DEFAULT,
464464
feed_import,
465465
feed_export,
466466
feed_load,
@@ -472,7 +472,6 @@ feed_get_node_type (void)
472472
feed_properties,
473473
feed_free
474474
};
475-
nti.icon = icon_get (ICON_DEFAULT);
476475

477476
return &nti;
478477
}

src/fl_sources/node_source.c

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
* @file node_source.c generic node source provider implementation
33
*
4-
* Copyright (C) 2005-2022 Lars Windolf <[email protected]>
4+
* Copyright (C) 2005-2023 Lars Windolf <[email protected]>
55
*
66
* This program is free software; you can redistribute it and/or modify
77
* it under the terms of the GNU General Public License as published by
@@ -622,7 +622,7 @@ node_source_get_node_type (void)
622622
/* derive the node source node type from the folder node type */
623623
nodeType = (nodeTypePtr) g_new0 (struct nodeType, 1);
624624
nodeType->id = "source";
625-
nodeType->icon = icon_get (ICON_DEFAULT);
625+
nodeType->icon = ICON_DEFAULT;
626626
nodeType->capabilities = NODE_CAPABILITY_SHOW_UNREAD_COUNT |
627627
NODE_CAPABILITY_SHOW_ITEM_FAVICONS |
628628
NODE_CAPABILITY_UPDATE_CHILDS |

src/folder.c

+2-3
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ folder_get_node_type (void)
119119
NODE_CAPABILITY_UPDATE_CHILDS |
120120
NODE_CAPABILITY_EXPORT,
121121
"folder",
122-
NULL,
122+
ICON_FOLDER,
123123
folder_import,
124124
folder_export,
125125
folder_load,
@@ -131,7 +131,6 @@ folder_get_node_type (void)
131131
feed_list_view_rename_node,
132132
NULL
133133
};
134-
fnti.icon = icon_get (ICON_FOLDER);
135134

136135
return &fnti;
137136
}
@@ -150,7 +149,7 @@ root_get_node_type (void)
150149
NODE_CAPABILITY_UPDATE_CHILDS |
151150
NODE_CAPABILITY_EXPORT,
152151
"root",
153-
NULL, /* and no need for an icon */
152+
0, /* and no need for an icon */
154153
folder_import,
155154
folder_export,
156155
folder_load,

src/html.c

+14-6
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ html_auto_discover_collect_meta (xmlNodePtr match, gpointer user_data)
221221
GSList *
222222
html_auto_discover_feed (const gchar* data, const gchar *defaultBaseUri)
223223
{
224-
GSList *iter, *links = NULL;
224+
GSList *iter, *links = NULL, *valid_links = NULL;
225225
gchar *baseUri = NULL;
226226
xmlDocPtr doc;
227227
xmlNodePtr node, root;
@@ -253,17 +253,25 @@ html_auto_discover_feed (const gchar* data, const gchar *defaultBaseUri)
253253
/* Turn relative URIs into absolute URIs */
254254
iter = links;
255255
while (iter) {
256-
gchar *tmp = iter->data;
257-
iter->data = common_build_url (tmp, baseUri);
258-
g_free (tmp);
259-
debug1 (DEBUG_UPDATE, "search result: %s", (gchar *)iter->data);
256+
gchar *tmp = (gchar *)common_build_url (iter->data, baseUri);
257+
258+
/* We expect only relative URIs starting with '/' or absolute URIs starting with 'http://' or 'https://' */
259+
if ('h' == tmp[0] || '/' == tmp[0]) {
260+
debug1 (DEBUG_UPDATE, "search result: %s", (gchar *)iter->data);
261+
valid_links = g_slist_append (valid_links, tmp);
262+
} else {
263+
debug1 (DEBUG_UPDATE, "html_auto_discover_feed: discarding invalid URL %s", tmp ? tmp : "NULL");
264+
g_free (tmp);
265+
}
266+
260267
iter = g_slist_next (iter);
261268
}
269+
g_slist_free_full (links, g_free);
262270

263271
g_free (baseUri);
264272
xmlFreeDoc (doc);
265273

266-
return links;
274+
return valid_links;
267275
}
268276

269277
GSList *

src/item.c

+3-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,9 @@ void
140140
item_set_source (LifereaItem *item, const gchar * source)
141141
{
142142
g_free (item->source);
143-
if (source)
143+
144+
/* We expect only relative URIs starting with '/' or absolute URIs starting with 'http://' or 'https://' */
145+
if (source && ('/' == source[0] || 'h' == source[0]))
144146
item->source = g_strstrip (g_strdup (source));
145147
else
146148
item->source = NULL;

src/newsbin.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ newsbin_get_node_type (void)
217217
NODE_CAPABILITY_SHOW_ITEM_COUNT |
218218
NODE_CAPABILITY_EXPORT_ITEMS;
219219
nodeType->id = "newsbin";
220-
nodeType->icon = icon_get (ICON_NEWSBIN);
220+
nodeType->icon = ICON_NEWSBIN;
221221
nodeType->load = feed_get_node_type()->load;
222222
nodeType->import = newsbin_import;
223223
nodeType->export = newsbin_export;

src/node.c

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
#include "date.h"
4444
#include "fl_sources/node_source.h"
4545
#include "ui/feed_list_view.h"
46+
#include "ui/icons.h"
4647
#include "ui/liferea_shell.h"
4748

4849
static GHashTable *nodes = NULL; /*<< node id -> node lookup table */
@@ -431,7 +432,7 @@ gpointer
431432
node_get_icon (nodePtr node)
432433
{
433434
if (!node->icon)
434-
return (gpointer) NODE_TYPE(node)->icon;
435+
return (gpointer) icon_get (NODE_TYPE(node)->icon);
435436

436437
return node->icon;
437438
}

src/node_type.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ enum {
5454
typedef struct nodeType {
5555
gulong capabilities; /**< bitmask of node type capabilities */
5656
const gchar *id; /**< type id (used for type attribute in OPML export) */
57-
const GIcon *icon; /**< default icon for nodes of this type (if no favicon available) */
57+
guint icon; /**< default icon for nodes of this type (if no favicon available) */
5858

5959
/* For method documentation see the wrappers defined below!
6060
All methods are mandatory for each node type. */

src/tests/Makefile.am

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
noinst_PROGRAMS = $(TEST_PROGS)
44

5-
TEST_PROGS = parse_html favicon parse_date parse_xml social
5+
TEST_PROGS = parse_html favicon parse_date parse_rss parse_xml social
66

77
test: $(TEST_PROGS)
88
echo $(TEST_PROGS) |\
@@ -93,6 +93,9 @@ parse_html_LDADD = $(favicon_LDADD)
9393
parse_date_CFLAGS = $(AM_CPPFLAGS)
9494
parse_date_LDADD = $(favicon_LDADD)
9595

96+
parse_rss_CFLAGS = $(AM_CPPFLAGS)
97+
parse_rss_LDADD = $(favicon_LDADD)
98+
9699
parse_xml_CFLAGS = $(AM_CPPFLAGS)
97100
parse_xml_LDADD = $(favicon_LDADD)
98101

src/tests/parse_html.c

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
2-
* @file html.c Test cases for feed link auto discovery
2+
* @file parse_html.c Test cases for feed link auto discovery
33
*
4-
* Copyright (C) 2014-2019 Lars Windolf <[email protected]>
4+
* Copyright (C) 2014-2023 Lars Windolf <[email protected]>
55
*
66
* This program is free software; you can redistribute it and/or modify
77
* it under the terms of the GNU General Public License as published by
@@ -20,6 +20,7 @@
2020

2121
#include <glib.h>
2222

23+
#include "debug.h"
2324
#include "html.h"
2425

2526
/* We need two groups of autodiscovery test cases, one for the tag soup fuzzy
@@ -115,6 +116,13 @@ gchar *tc_xml_atom3[] = {
115116
NULL
116117
};
117118

119+
// Injection via "|"" command must not result in command subscription
120+
gchar *tc_xml_rce[] = {
121+
"<html><head><link rel=\"alternate\" type=\"application/rss+xml\" href=\"|date &gt;/tmp/bad-feed-discovery.txt\"></html>",
122+
NULL,
123+
NULL
124+
};
125+
118126
/* HTML5 extraction test cases */
119127

120128
gchar *tc_article[] = {
@@ -214,6 +222,9 @@ main (int argc, char *argv[])
214222
{
215223
g_test_init (&argc, &argv, NULL);
216224

225+
if (argv[1] && g_str_equal (argv[1], "--debug"))
226+
set_debug_level (DEBUG_UPDATE | DEBUG_HTML | DEBUG_PARSING);
227+
217228
g_test_add_data_func ("/html/auto_discover_link_xml", &tc_xml, &tc_auto_discover_link);
218229
g_test_add_data_func ("/html/auto_discover_link_xml_base_url", &tc_xml_base_url, &tc_auto_discover_link);
219230
g_test_add_data_func ("/html/auto_discover_link_rss", &tc_rss, &tc_auto_discover_link);
@@ -225,6 +236,7 @@ main (int argc, char *argv[])
225236
g_test_add_data_func ("/html/auto_discover_link_xml_atom", &tc_xml_atom, &tc_auto_discover_link);
226237
g_test_add_data_func ("/html/auto_discover_link_xml_atom2", &tc_xml_atom2, &tc_auto_discover_link);
227238
g_test_add_data_func ("/html/auto_discover_link_xml_atom3", &tc_xml_atom3, &tc_auto_discover_link);
239+
g_test_add_data_func ("/html/auto_discover_link_xml_rce", &tc_xml_rce, &tc_auto_discover_link);
228240

229241
g_test_add_data_func ("/html/html5_extract_article", &tc_article, &tc_get_article);
230242
g_test_add_data_func ("/html/html5_extract_article_main", &tc_article_main, &tc_get_article);

src/tests/parse_rss.c

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* @file parse_rss.c Test cases for RSS parsing
3+
*
4+
* Copyright (C) 2023 Lars Windolf <[email protected]>
5+
*
6+
* This program is free software; you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation; either version 2 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program; if not, write to the Free Software
18+
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19+
*/
20+
21+
#include <glib.h>
22+
#include <string.h>
23+
24+
#include "debug.h"
25+
#include "feed.h"
26+
#include "feed_parser.h"
27+
#include "item.h"
28+
#include "subscription.h"
29+
#include "xml.h"
30+
31+
/* Format of test cases:
32+
33+
1. feed XML string
34+
2. "true" for successfully parsed feed, "false" for unparseable
35+
3. number of items
36+
4..n string of XML serialized items
37+
*/
38+
39+
gchar *tc_rss_feed1[] = {
40+
"<rss version=\"2.0\"><channel><title>T</title><link>http://localhost</link><item><title>i1</title><link>http://localhost/item1.html</link><description>D</description></item><item><title>i2</title><link>https://localhost/item2.html</link></item></channel></rss>",
41+
"true",
42+
"2",
43+
"<item><title>i1</title><description>&lt;div xmlns=\"http://www.w3.org/1999/xhtml\"&gt;&lt;p&gt;D&lt;/p&gt;&lt;/div&gt;</description><source>http://localhost/item1.html</source><nr>0</nr><readStatus>0</readStatus><updateStatus>0</updateStatus><mark>0</mark><time>1678397817</time><sourceId/><sourceNr>0</sourceNr><attributes/></item>",
44+
"<item><title>i2</title><source>https://localhost/item2.html</source><nr>0</nr><readStatus>0</readStatus><updateStatus>0</updateStatus><mark>0</mark><time>1678397817</time><sourceId/><sourceNr>0</sourceNr><attributes/></item>",
45+
NULL
46+
};
47+
48+
/* Test case to prevent | command injection in item link which could trigger
49+
a HTML5 extraction */
50+
gchar *tc_rss_feed2_rce[] = {
51+
"<rss version=\"2.0\"><channel><title>T</title><item><title>i1</title><link>|date >/tmp/bad-item-link.txt</link></item></channel></rss>",
52+
"true",
53+
"1",
54+
"<item><title>i1</title><nr>0</nr><readStatus>0</readStatus><updateStatus>0</updateStatus><mark>0</mark><time>1678397817</time><sourceId/><sourceNr>0</sourceNr><attributes/></item>",
55+
NULL
56+
};
57+
58+
static void
59+
tc_parse_feed (gconstpointer user_data)
60+
{
61+
gchar **tc = (gchar **)user_data;
62+
nodePtr node;
63+
feedParserCtxtPtr ctxt;
64+
int i;
65+
GList *iter;
66+
67+
node = node_new (feed_get_node_type ());
68+
node_set_data (node, feed_new ());
69+
node_set_subscription (node, subscription_new (NULL, NULL, NULL));
70+
ctxt = feed_parser_ctxt_new (node->subscription, tc[0], strlen(tc[0]));
71+
72+
g_assert_cmpstr (feed_parse (ctxt)?"true":"false", ==, tc[1]);
73+
g_assert (g_list_length (ctxt->items) == atoi(tc[2]));
74+
75+
i = 2;
76+
iter = ctxt->items;
77+
while (tc[++i]) {
78+
gchar *buffer, *tmp, *tmp2;
79+
gint buffersize;
80+
xmlDocPtr doc = xmlNewDoc (BAD_CAST"1.0");
81+
xmlNodePtr rootNode = xmlNewDocNode (doc, NULL, BAD_CAST"result", NULL);
82+
83+
xmlDocSetRootElement (doc, rootNode);
84+
85+
// Force time and delete <timestr> to make result compareable
86+
itemPtr item = (itemPtr)iter->data;
87+
item->time = 1678397817;
88+
item_to_xml (item, rootNode);
89+
90+
xmlNode *timestr = xpath_find (rootNode, "//timestr");
91+
if (timestr) {
92+
xmlUnlinkNode (timestr);
93+
xmlFreeNode (timestr);
94+
}
95+
xmlDocDumpMemory(doc, (xmlChar **)&buffer, &buffersize);
96+
97+
/* strip boilerplate */
98+
tmp = buffer;
99+
if ((tmp = strstr (tmp, "<result>")))
100+
tmp += 8;
101+
if ((tmp2 = strstr (tmp, "</result>")))
102+
*tmp2 = 0;
103+
104+
g_assert_cmpstr (tc[i], ==, tmp);
105+
106+
xmlFreeDoc (doc);
107+
xmlFree (buffer);
108+
109+
iter = g_list_next (iter);
110+
}
111+
112+
feed_parser_ctxt_free (ctxt);
113+
node_free (node);
114+
}
115+
116+
int
117+
main (int argc, char *argv[])
118+
{
119+
g_test_init (&argc, &argv, NULL);
120+
121+
if (argv[1] && g_str_equal (argv[1], "--debug"))
122+
set_debug_level (DEBUG_UPDATE | DEBUG_HTML | DEBUG_PARSING);
123+
124+
g_test_add_data_func ("/rss/feed1", &tc_rss_feed1, &tc_parse_feed);
125+
g_test_add_data_func ("/rss/feed2_rce", &tc_rss_feed2_rce, &tc_parse_feed);
126+
127+
return g_test_run();
128+
}

src/vfolder.c

+1-2
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ vfolder_get_node_type (void)
308308
NODE_CAPABILITY_SHOW_UNREAD_COUNT |
309309
NODE_CAPABILITY_EXPORT_ITEMS,
310310
"vfolder",
311-
NULL,
311+
ICON_VFOLDER,
312312
vfolder_import,
313313
vfolder_export,
314314
vfolder_load,
@@ -320,7 +320,6 @@ vfolder_get_node_type (void)
320320
vfolder_properties,
321321
vfolder_free
322322
};
323-
nti.icon = icon_get (ICON_VFOLDER);
324323

325324
return &nti;
326325
}

0 commit comments

Comments
 (0)