Merge lp:~jamesh/thumbnailer/aa-access-fix into lp:thumbnailer/devel

Proposed by James Henstridge
Status: Merged
Approved by: Michi Henning
Approved revision: 220
Merged at revision: 219
Proposed branch: lp:~jamesh/thumbnailer/aa-access-fix
Merge into: lp:thumbnailer/devel
Diff against target: 315 lines (+50/-44)
4 files modified
include/internal/thumbnailer.h (+3/-2)
src/service/handler.cpp (+10/-1)
src/thumbnailer.cpp (+19/-18)
tests/thumbnailer/thumbnailer_test.cpp (+18/-23)
To merge this branch: bzr merge lp:~jamesh/thumbnailer/aa-access-fix
Reviewer Review Type Date Requested Status
PS Jenkins bot (community) continuous-integration Approve
Michi Henning (community) Approve
Review via email: mp+262295@code.launchpad.net

Commit message

Move the AppArmor check earlier so it is not skipped for files already in the cache.

Description of the change

With the previous AppArmor changes, I moved the GetThumbnail() security check to the LocalThumbnailRequest::fetch() method. Unfortunately this method is not called if there is a cached version of the thumbnail, giving clients access to data they shouldn't have.

This branch renames the ThumbnailRequest::set_client_credentials() method to check_client_credentials(), and performs the access checks from within this method. This code path is always hit as part of the request, so avoids the problem.

Unfortunately, I can't easily check the AppArmor aspects from within the test suite since (a) AppArmor isn't available within the Jenkins environment, and (b) even if it was, it isn't clear what profiles we can rely on being loaded into the kernel.

Once I'm done with manual testing, I'll try to formulate this as something people can repeat in the test plan. The thumbnailer-admin utility is really useful here.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Michi Henning (michihenning) wrote :

Looks good! One comment needs a fix (see inline).

review: Needs Fixing
lp:~jamesh/thumbnailer/aa-access-fix updated
220. By James Henstridge

Fix stale comment, as per Michi's review.

Revision history for this message
Michi Henning (michihenning) wrote :

Thank you!

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'include/internal/thumbnailer.h'
2--- include/internal/thumbnailer.h 2015-06-16 10:16:03 +0000
3+++ include/internal/thumbnailer.h 2015-06-18 07:19:26 +0000
4@@ -73,8 +73,9 @@
5
6 virtual std::string const& key() const = 0;
7
8- // Set the credentials of the caller.
9- virtual void set_client_credentials(uid_t user, std::string const& apparmor_label) = 0;
10+ // Check that the client has access to the thumbnail. Throws an
11+ // exception on authentication failure.
12+ virtual void check_client_credentials(uid_t user, std::string const& apparmor_label) = 0;
13
14 Q_SIGNALS:
15 void downloadFinished();
16
17=== modified file 'src/service/handler.cpp'
18--- src/service/handler.cpp 2015-06-16 10:16:03 +0000
19+++ src/service/handler.cpp 2015-06-18 07:19:26 +0000
20@@ -201,7 +201,16 @@
21 sendError("gotCredentials(): " + details() + ": could not retrieve peer credentials");
22 return;
23 }
24- p->request->set_client_credentials(credentials.user, credentials.label);
25+ try
26+ {
27+ p->request->check_client_credentials(
28+ credentials.user, credentials.label);
29+ }
30+ catch (std::exception const& e)
31+ {
32+ sendError("gotCredentials(): " + details() + ": " + e.what());
33+ return;
34+ }
35
36 auto do_check = [this]() -> FdOrError
37 {
38
39=== modified file 'src/thumbnailer.cpp'
40--- src/thumbnailer.cpp 2015-06-17 03:58:34 +0000
41+++ src/thumbnailer.cpp 2015-06-18 07:19:26 +0000
42@@ -77,10 +77,8 @@
43 {
44 return key_;
45 }
46- void set_client_credentials(uid_t user, std::string const& label) override
47+ void check_client_credentials(uid_t, std::string const&) override
48 {
49- client_user_ = user;
50- client_apparmor_label_ = label;
51 }
52 enum class CachePolicy
53 {
54@@ -144,8 +142,6 @@
55 string key_;
56 QSize const requested_size_;
57 chrono::milliseconds timeout_;
58- uid_t client_user_ = 0;
59- string client_apparmor_label_;
60
61 private:
62 FetchStatus status_;
63@@ -162,6 +158,7 @@
64 string const& filename,
65 QSize const& requested_size,
66 chrono::milliseconds timeout);
67+ void check_client_credentials(uid_t user, std::string const& label) override;
68
69 protected:
70 ImageData fetch(QSize const& size_hint) override;
71@@ -415,6 +412,23 @@
72 key_ += to_string(st.st_ctim.tv_sec) + "." + to_string(st.st_ctim.tv_nsec);
73 }
74
75+void LocalThumbnailRequest::check_client_credentials(uid_t user,
76+ std::string const& label)
77+{
78+ if (user != geteuid())
79+ {
80+ throw runtime_error("LocalThumbnailRequest::fetch(): Request comes from a different user ID");
81+ }
82+ if (!apparmor_can_read(label, filename_)) {
83+ // LCOV_EXCL_START
84+ qDebug() << "Apparmor label" << QString::fromStdString(label) << "has no access to" << QString::fromStdString(filename_);
85+ throw runtime_error("LocalThumbnailRequest::fetch(): AppArmor policy forbids access to " + filename_);
86+ // LCOV_EXCL_STOP
87+ }
88+
89+}
90+
91+
92 RequestBase::ImageData LocalThumbnailRequest::fetch(QSize const& size_hint)
93 {
94 if (image_extractor_)
95@@ -423,19 +437,6 @@
96 return ImageData(Image(image_extractor_->data()), CachePolicy::cache_fullsize, Location::local);
97 }
98
99- if (client_user_ != geteuid())
100- {
101- // LCOV_EXCL_START
102- throw runtime_error("LocalThumbnailRequest::fetch(): Request comes from a different user ID");
103- // LCOV_EXCL_STOP
104- }
105- if (!apparmor_can_read(client_apparmor_label_, filename_)) {
106- // LCOV_EXCL_START
107- qDebug() << "Apparmor label" << QString::fromStdString(client_apparmor_label_) << "has no access to" << QString::fromStdString(filename_);
108- throw runtime_error("LocalThumbnailRequest::fetch(): AppArmor policy forbids access to " + filename_);
109- // LCOV_EXCL_STOP
110- }
111-
112 // Work out content type.
113 gobj_ptr<GFile> file(g_file_new_for_path(filename_.c_str()));
114 assert(file); // Cannot fail according to doc.
115
116=== modified file 'tests/thumbnailer/thumbnailer_test.cpp'
117--- tests/thumbnailer/thumbnailer_test.cpp 2015-06-16 09:37:33 +0000
118+++ tests/thumbnailer/thumbnailer_test.cpp 2015-06-18 07:19:26 +0000
119@@ -93,18 +93,15 @@
120 Image img;
121
122 request = tn.get_thumbnail(EMPTY_IMAGE, QSize());
123- request->set_client_credentials(geteuid(), "unconfined");
124 thumb = request->thumbnail();
125 EXPECT_EQ("", thumb);
126
127 // Again, this time we get the answer from the failure cache.
128 request = tn.get_thumbnail(EMPTY_IMAGE, QSize());
129- request->set_client_credentials(geteuid(), "unconfined");
130 thumb = request->thumbnail();
131 EXPECT_EQ("", thumb);
132
133 request = tn.get_thumbnail(TEST_IMAGE, QSize());
134- request->set_client_credentials(geteuid(), "unconfined");
135 EXPECT_TRUE(boost::starts_with(request->key(), TEST_IMAGE)) << request->key();
136 thumb = request->thumbnail();
137 img = Image(thumb);
138@@ -113,28 +110,24 @@
139
140 // Again, for coverage. This time the thumbnail comes from the cache.
141 request = tn.get_thumbnail(TEST_IMAGE, QSize());
142- request->set_client_credentials(geteuid(), "unconfined");
143 thumb = request->thumbnail();
144 img = Image(thumb);
145 EXPECT_EQ(640, img.width());
146 EXPECT_EQ(480, img.height());
147
148 request = tn.get_thumbnail(TEST_IMAGE, QSize(160, 160));
149- request->set_client_credentials(geteuid(), "unconfined");
150 thumb = request->thumbnail();
151 img = Image(thumb);
152 EXPECT_EQ(160, img.width());
153 EXPECT_EQ(120, img.height());
154
155 request = tn.get_thumbnail(TEST_IMAGE, QSize(1000, 1000)); // Will not up-scale
156- request->set_client_credentials(geteuid(), "unconfined");
157 thumb = request->thumbnail();
158 img = Image(thumb);
159 EXPECT_EQ(640, img.width());
160 EXPECT_EQ(480, img.height());
161
162 request = tn.get_thumbnail(TEST_IMAGE, QSize(100, 100)); // From EXIF data
163- request->set_client_credentials(geteuid(), "unconfined");
164 thumb = request->thumbnail();
165 img = Image(thumb);
166 EXPECT_EQ(100, img.width());
167@@ -143,7 +136,6 @@
168 try
169 {
170 request = tn.get_thumbnail(BAD_IMAGE, QSize());
171- request->set_client_credentials(geteuid(), "unconfined");
172 request->thumbnail();
173 FAIL();
174 }
175@@ -154,21 +146,18 @@
176 }
177
178 request = tn.get_thumbnail(RGB_IMAGE, QSize(48, 48));
179- request->set_client_credentials(geteuid(), "unconfined");
180 thumb = request->thumbnail();
181 img = Image(thumb);
182 EXPECT_EQ(48, img.width());
183 EXPECT_EQ(48, img.height());
184
185 request = tn.get_thumbnail(BIG_IMAGE, QSize()); // > 1920, so will be trimmed down
186- request->set_client_credentials(geteuid(), "unconfined");
187 thumb = request->thumbnail();
188 img = Image(thumb);
189 EXPECT_EQ(1920, img.width());
190 EXPECT_EQ(1439, img.height());
191
192 request = tn.get_thumbnail(BIG_IMAGE, QSize(0, 0)); // unconstrained, so will not be trimmed down
193- request->set_client_credentials(geteuid(), "unconfined");
194 thumb = request->thumbnail();
195 img = Image(thumb);
196 EXPECT_EQ(2731, img.width());
197@@ -200,7 +189,6 @@
198 // Load a song so we have something in the full-size and thumbnail caches.
199 auto request = tn.get_thumbnail(TEST_SONG, QSize());
200 ASSERT_NE(nullptr, request.get());
201- request->set_client_credentials(geteuid(), "unconfined");
202 // Audio thumbnails cannot be produced immediately
203 ASSERT_EQ("", request->thumbnail());
204
205@@ -218,7 +206,6 @@
206 // Load same song again at different size, so we get a hit on full-size cache.
207 auto request = tn.get_thumbnail(TEST_SONG, QSize(20, 20));
208 ASSERT_NE(nullptr, request.get());
209- request->set_client_credentials(geteuid(), "unconfined");
210 ASSERT_NE("", request->thumbnail());
211 }
212
213@@ -226,21 +213,18 @@
214 // Load same song again at same size, so we get a hit on thumbnail cache.
215 auto request = tn.get_thumbnail(TEST_SONG, QSize(20, 20));
216 ASSERT_NE(nullptr, request.get());
217- request->set_client_credentials(geteuid(), "unconfined");
218 ASSERT_NE("", request->thumbnail());
219 }
220
221 {
222 // Load an empty image, so we have something in the failure cache.
223 auto request = tn.get_thumbnail(EMPTY_IMAGE, QSize());
224- request->set_client_credentials(geteuid(), "unconfined");
225 EXPECT_EQ("", request->thumbnail());
226 }
227
228 {
229 // Load empty image again, so we get a hit on failure cache.
230 auto request = tn.get_thumbnail(EMPTY_IMAGE, QSize());
231- request->set_client_credentials(geteuid(), "unconfined");
232 EXPECT_EQ("", request->thumbnail());
233 }
234 };
235@@ -340,7 +324,6 @@
236 ASSERT_EQ(0, unlink(testfile.c_str()));
237 ASSERT_EQ(0, link(BIG_IMAGE, testfile.c_str()));
238
239- request->set_client_credentials(geteuid(), "unconfined");
240 string data = request->thumbnail();
241 Image img(data);
242 EXPECT_EQ(640, img.width());
243@@ -352,7 +335,6 @@
244 Thumbnailer tn;
245 auto request = tn.get_thumbnail(TEST_VIDEO, QSize());
246 ASSERT_NE(nullptr, request.get());
247- request->set_client_credentials(geteuid(), "unconfined");
248 // Video thumbnails cannot be produced immediately
249 ASSERT_EQ("", request->thumbnail());
250
251@@ -371,7 +353,6 @@
252 // Fetch the thumbnail again with the same size.
253 // That causes it to come from the thumbnail cache.
254 auto request = tn.get_thumbnail(TEST_VIDEO, QSize());
255- request->set_client_credentials(geteuid(), "unconfined");
256 string thumb = request->thumbnail();
257 ASSERT_NE("", thumb);
258 Image img(thumb);
259@@ -383,7 +364,6 @@
260 // Fetch the thumbnail again with a different size.
261 // That causes it to be scaled from the thumbnail cache.
262 auto request = tn.get_thumbnail(TEST_VIDEO, QSize(500, 500));
263- request->set_client_credentials(geteuid(), "unconfined");
264 string thumb = request->thumbnail();
265 ASSERT_NE("", thumb);
266 Image img(thumb);
267@@ -399,7 +379,6 @@
268
269 Thumbnailer tn;
270 auto request = tn.get_thumbnail(testfile, QSize());
271- request->set_client_credentials(geteuid(), "unconfined");
272 ASSERT_EQ("", request->thumbnail());
273
274 // Replace test image with a different file with different
275@@ -422,7 +401,6 @@
276 Thumbnailer tn;
277 auto request = tn.get_thumbnail(TEST_SONG, QSize());
278 ASSERT_NE(nullptr, request.get());
279- request->set_client_credentials(geteuid(), "unconfined");
280 // Audio thumbnails cannot be produced immediately
281 ASSERT_EQ("", request->thumbnail());
282
283@@ -467,7 +445,6 @@
284 setenv("TN_UTILDIR", "no_such_directory", true);
285
286 auto request = tn.get_thumbnail(TEST_SONG, QSize());
287- request->set_client_credentials(geteuid(), "unconfined");
288 EXPECT_EQ("", request->thumbnail());
289
290 QSignalSpy spy(request.get(), &ThumbnailRequest::downloadFinished);
291@@ -489,6 +466,24 @@
292 }
293 }
294
295+TEST_F(ThumbnailerTest, check_client_access)
296+{
297+ Thumbnailer tn;
298+ auto request = tn.get_thumbnail(TEST_IMAGE, QSize());
299+ ASSERT_NE(nullptr, request.get());
300+ // Check succeeds for correct user ID and valid label
301+ request->check_client_credentials(geteuid(), "unconfined");
302+ try
303+ {
304+ request->check_client_credentials(geteuid() + 1, "unconfined");
305+ FAIL();
306+ }
307+ catch (std::exception const& e)
308+ {
309+ EXPECT_TRUE(boost::contains(e.what(), "Request comes from a different user ID")) << e.what();
310+ }
311+}
312+
313 class RemoteServer : public ThumbnailerTest
314 {
315 protected:

Subscribers

People subscribed via source and target branches

to all changes: