Skip to content

Commit fd3717a

Browse files
committed
SpawningKit: ensure safe reading of files in the work dir after its finalization
After finalization of the work dir, it is owned by the app's user and group. This allows the app to arbitrarily modify the work dir to insert symlinks. In order to prevent the SpawningKit code (which may run as root) from reading arbitrary files through symlink attacks, we use the safeReadFile() function. This function also mitigates various DoS attacks (see its comments).
1 parent 3e882a1 commit fd3717a

File tree

8 files changed

+180
-40
lines changed

8 files changed

+180
-40
lines changed

src/agent/Core/SpawningKit/DirectSpawner.h

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141

4242
#include <limits.h> // for PTHREAD_STACK_MIN
4343
#include <pthread.h>
44+
#include <unistd.h>
4445
#include <adhoc_lve.h>
4546

4647
namespace Passenger {

src/agent/Core/SpawningKit/Handshake/Perform.h

+56-27
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* Phusion Passenger - https://www.phusionpassenger.com/
3-
* Copyright (c) 2016-2017 Phusion Holding B.V.
3+
* Copyright (c) 2016-2018 Phusion Holding B.V.
44
*
55
* "Passenger", "Phusion Passenger" and "Union Station" are registered
66
* trademarks of Phusion Holding B.V.
@@ -147,7 +147,8 @@ class HandshakePerform {
147147
TRACE_POINT();
148148
try {
149149
string path = session.responseDir + "/finish";
150-
int fd = syscalls::open(path.c_str(), O_RDONLY);
150+
int fd = syscalls::openat(session.responseDirFd, "finish",
151+
O_RDONLY | O_NOFOLLOW);
151152
if (fd == -1) {
152153
int e = errno;
153154
throw FileSystemException("Error opening FIFO " + path,
@@ -378,13 +379,21 @@ class HandshakePerform {
378379
vector<string> errors;
379380

380381
// We already checked whether properties.json exists before invoking
381-
// this method, so if unsafeReadFile() fails then we can't be sure that
382+
// this method, so if safeReadFile() fails then we can't be sure that
382383
// it's an application problem. This is why we want the SystemException
383384
// to propagate to higher layers so that there it can be turned into
384385
// a generic filesystem-related or IO-related SpawnException, as opposed
385386
// to one about this problem specifically.
386387

387-
if (!reader.parse(unsafeReadFile(path), doc)) {
388+
pair<string, bool> jsonContent = safeReadFile(session.responseDirFd, "properties.json",
389+
SPAWNINGKIT_MAX_PROPERTIES_JSON_SIZE);
390+
if (!jsonContent.second) {
391+
errors.push_back("Error parsing " + path + ": file bigger than "
392+
+ toString(SPAWNINGKIT_MAX_PROPERTIES_JSON_SIZE) + " bytes");
393+
throwSpawnExceptionBecauseOfResultValidationErrors(vector<string>(),
394+
errors);
395+
}
396+
if (!reader.parse(jsonContent.first, doc)) {
388397
errors.push_back("Error parsing " + path + ": " +
389398
reader.getFormattedErrorMessages());
390399
throwSpawnExceptionBecauseOfResultValidationErrors(vector<string>(),
@@ -923,7 +932,8 @@ class HandshakePerform {
923932
ErrorCategory inferErrorCategoryFromResponseDir(ErrorCategory defaultValue) const {
924933
TRACE_POINT();
925934
if (fileExists(session.responseDir + "/error/category")) {
926-
string value = strip(unsafeReadFile(session.responseDir + "/error/category"));
935+
string value = strip(safeReadFile(session.responseErrorDirFd,
936+
"category", SPAWNINGKIT_MAX_ERROR_CATEGORY_SIZE).first);
927937
ErrorCategory category = stringToErrorCategory(value);
928938

929939
if (category == UNKNOWN_ERROR_CATEGORY) {
@@ -1048,18 +1058,24 @@ class HandshakePerform {
10481058
continue;
10491059
}
10501060

1061+
map<JourneyStep, int>::const_iterator it = session.stepDirFds.find(step);
1062+
if (it == session.stepDirFds.end()) {
1063+
P_BUG("No fd opened for step " << stepString);
1064+
}
1065+
10511066
loadJourneyStateFromResponseDirForSpecificStep(
1052-
session, pid, stdoutAndErrCapturer, step, stepDir);
1067+
session, pid, stdoutAndErrCapturer, step, stepDir, it->second);
10531068
}
10541069
}
10551070

10561071
static void loadJourneyStateFromResponseDirForSpecificStep(HandshakeSession &session,
10571072
pid_t pid, const BackgroundIOCapturerPtr &stdoutAndErrCapturer,
1058-
JourneyStep step, const string &stepDir)
1073+
JourneyStep step, const string &stepDir, int stepDirFd)
10591074
{
10601075
TRACE_POINT_WITH_DATA(journeyStepToString(step).data());
10611076
string summary;
1062-
string value = strip(unsafeReadFile(stepDir + "/state"));
1077+
string value = strip(safeReadFile(stepDirFd, "state",
1078+
SPAWNINGKIT_MAX_JOURNEY_STEP_FILE_SIZE).first);
10631079
JourneyStepState state = stringToJourneyStepState(value);
10641080
const Config *config = session.config;
10651081

@@ -1275,13 +1291,15 @@ class HandshakePerform {
12751291

12761292
UPDATE_TRACE_POINT();
12771293
if (fileExists(stepDir + "/begin_time_monotonic")) {
1278-
value = unsafeReadFile(stepDir + "/begin_time_monotonic");
1294+
value = safeReadFile(stepDirFd, "begin_time_monotonic",
1295+
SPAWNINGKIT_MAX_JOURNEY_STEP_FILE_SIZE).first;
12791296
MonotonicTimeUsec beginTimeMonotonic = atof(value.c_str()) * 1000000;
12801297
P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step)
12811298
<< ": monotonic begin time is \"" << cEscapeString(value) << "\"");
12821299
session.journey.setStepBeginTime(step, beginTimeMonotonic);
12831300
} else if (fileExists(stepDir + "/begin_time")) {
1284-
value = unsafeReadFile(stepDir + "/begin_time");
1301+
value = safeReadFile(stepDirFd, "begin_time",
1302+
SPAWNINGKIT_MAX_JOURNEY_STEP_FILE_SIZE).first;
12851303
unsigned long long beginTime = atof(value.c_str()) * 1000000;
12861304
MonotonicTimeUsec beginTimeMonotonic = usecTimestampToMonoTime(beginTime);
12871305
P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step)
@@ -1295,13 +1313,15 @@ class HandshakePerform {
12951313

12961314
UPDATE_TRACE_POINT();
12971315
if (fileExists(stepDir + "/end_time_monotonic")) {
1298-
value = unsafeReadFile(stepDir + "/end_time_monotonic");
1316+
value = safeReadFile(stepDirFd, "end_time_monotonic",
1317+
SPAWNINGKIT_MAX_JOURNEY_STEP_FILE_SIZE).first;
12991318
MonotonicTimeUsec endTimeMonotonic = atof(value.c_str()) * 1000000;
13001319
P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step)
13011320
<< ": monotonic end time is \"" << cEscapeString(value) << "\"");
13021321
session.journey.setStepEndTime(step, endTimeMonotonic);
13031322
} else if (fileExists(stepDir + "/end_time")) {
1304-
value = unsafeReadFile(stepDir + "/end_time");
1323+
value = safeReadFile(stepDirFd, "end_time",
1324+
SPAWNINGKIT_MAX_JOURNEY_STEP_FILE_SIZE).first;
13051325
unsigned long long endTime = atof(value.c_str()) * 1000000;
13061326
MonotonicTimeUsec endTimeMonotonic = usecTimestampToMonoTime(endTime);
13071327
P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step)
@@ -1334,32 +1354,36 @@ class HandshakePerform {
13341354
const string &envDumpDir = session.envDumpDir;
13351355

13361356
if (fileExists(responseDir + "/error/summary")) {
1337-
e.setSummary(strip(unsafeReadFile(responseDir + "/error/summary")));
1357+
e.setSummary(strip(safeReadFile(session.responseErrorDirFd, "summary",
1358+
SPAWNINGKIT_MAX_SUBPROCESS_ERROR_MESSAGE_SIZE).first));
13381359
}
13391360

13401361
if (e.getAdvancedProblemDetails().empty()
13411362
&& fileExists(responseDir + "/error/advanced_problem_details"))
13421363
{
1343-
e.setAdvancedProblemDetails(strip(unsafeReadFile(responseDir
1344-
+ "/error/advanced_problem_details")));
1364+
e.setAdvancedProblemDetails(strip(safeReadFile(session.responseErrorDirFd,
1365+
"advanced_problem_details", SPAWNINGKIT_MAX_SUBPROCESS_ERROR_MESSAGE_SIZE).first));
13451366
}
13461367

13471368
if (fileExists(responseDir + "/error/problem_description.html")) {
1348-
e.setProblemDescriptionHTML(unsafeReadFile(responseDir + "/error/problem_description.html"));
1369+
e.setProblemDescriptionHTML(safeReadFile(session.responseErrorDirFd,
1370+
"problem_description.html", SPAWNINGKIT_MAX_SUBPROCESS_ERROR_MESSAGE_SIZE).first);
13491371
} else if (fileExists(responseDir + "/error/problem_description.txt")) {
1350-
e.setProblemDescriptionHTML(escapeHTML(strip(unsafeReadFile(
1351-
responseDir + "/error/problem_description.txt"))));
1372+
e.setProblemDescriptionHTML(escapeHTML(strip(safeReadFile(session.responseErrorDirFd,
1373+
"problem_description.txt", SPAWNINGKIT_MAX_SUBPROCESS_ERROR_MESSAGE_SIZE).first)));
13521374
}
13531375

13541376
if (fileExists(responseDir + "/error/solution_description.html")) {
1355-
e.setSolutionDescriptionHTML(unsafeReadFile(responseDir + "/error/solution_description.html"));
1377+
e.setSolutionDescriptionHTML(safeReadFile(session.responseErrorDirFd,
1378+
"solution_description.html", SPAWNINGKIT_MAX_SUBPROCESS_ERROR_MESSAGE_SIZE).first);
13561379
} else if (fileExists(responseDir + "/error/solution_description.txt")) {
1357-
e.setSolutionDescriptionHTML(escapeHTML(strip(unsafeReadFile(
1358-
responseDir + "/error/solution_description.txt"))));
1380+
e.setSolutionDescriptionHTML(escapeHTML(strip(safeReadFile(session.responseErrorDirFd,
1381+
"solution_description.txt", SPAWNINGKIT_MAX_SUBPROCESS_ERROR_MESSAGE_SIZE).first)));
13591382
}
13601383

13611384
string envvars, userInfo, ulimits;
1362-
loadBasicInfoFromEnvDumpDir(envDumpDir, envvars, userInfo, ulimits);
1385+
loadBasicInfoFromEnvDumpDir(envDumpDir, session.envDumpDirFd, envvars,
1386+
userInfo, ulimits);
13631387
e.setSubprocessEnvvars(envvars);
13641388
e.setSubprocessUserInfo(userInfo);
13651389
e.setSubprocessUlimits(ulimits);
@@ -1388,7 +1412,9 @@ class HandshakePerform {
13881412
while ((ent = readdir(dir)) != NULL) {
13891413
if (ent->d_name[0] != '.') {
13901414
e.setAnnotation(ent->d_name, strip(
1391-
unsafeReadFile(path + "/" + ent->d_name)));
1415+
safeReadFile(session.envDumpAnnotationsDirFd,
1416+
ent->d_name, SPAWNINGKIT_MAX_SUBPROCESS_ENVDUMP_SIZE).first
1417+
));
13921418
}
13931419
}
13941420
}
@@ -1630,16 +1656,19 @@ class HandshakePerform {
16301656
}
16311657

16321658
static void loadBasicInfoFromEnvDumpDir(const string &envDumpDir,
1633-
string &envvars, string &userInfo, string &ulimits)
1659+
int envDumpDirFd, string &envvars, string &userInfo, string &ulimits)
16341660
{
16351661
if (fileExists(envDumpDir + "/envvars")) {
1636-
envvars = unsafeReadFile(envDumpDir + "/envvars");
1662+
envvars = safeReadFile(envDumpDirFd, "envvars",
1663+
SPAWNINGKIT_MAX_SUBPROCESS_ENVDUMP_SIZE).first;
16371664
}
16381665
if (fileExists(envDumpDir + "/user_info")) {
1639-
userInfo = unsafeReadFile(envDumpDir + "/user_info");
1666+
userInfo = safeReadFile(envDumpDirFd, "user_info",
1667+
SPAWNINGKIT_MAX_SUBPROCESS_ENVDUMP_SIZE).first;
16401668
}
16411669
if (fileExists(envDumpDir + "/ulimits")) {
1642-
ulimits = unsafeReadFile(envDumpDir + "/ulimits");
1670+
ulimits = safeReadFile(envDumpDirFd, "ulimits",
1671+
SPAWNINGKIT_MAX_SUBPROCESS_ENVDUMP_SIZE).first;
16431672
}
16441673
}
16451674
};

src/agent/Core/SpawningKit/Handshake/Prepare.h

+40-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* Phusion Passenger - https://www.phusionpassenger.com/
3-
* Copyright (c) 2016-2017 Phusion Holding B.V.
3+
* Copyright (c) 2016-2018 Phusion Holding B.V.
44
*
55
* "Passenger", "Phusion Passenger" and "Union Station" are registered
66
* trademarks of Phusion Holding B.V.
@@ -33,6 +33,7 @@
3333
#include <vector>
3434
#include <stdexcept>
3535
#include <algorithm>
36+
#include <utility>
3637
#include <cerrno>
3738
#include <cstddef>
3839
#include <cassert>
@@ -216,6 +217,43 @@ class HandshakePrepare {
216217
}
217218
}
218219

220+
// Open various workdir subdirectories because we'll use these file descriptors later in
221+
// safeReadFile() calls.
222+
void openWorkDirSubdirFds() {
223+
session.workDirFd = openDirFd(session.workDir->getPath());
224+
session.responseDirFd = openDirFd(session.responseDir);
225+
session.responseErrorDirFd = openDirFd(session.responseDir + "/error");
226+
session.envDumpDirFd = openDirFd(session.envDumpDir);
227+
session.envDumpAnnotationsDirFd = openDirFd(session.envDumpDir + "/annotations");
228+
openJourneyStepDirFds(getFirstSubprocessJourneyStep(),
229+
getLastSubprocessJourneyStep());
230+
openJourneyStepDirFds(getFirstPreloaderJourneyStep(),
231+
JourneyStep((int) getLastPreloaderJourneyStep() + 1));
232+
}
233+
234+
void openJourneyStepDirFds(JourneyStep firstStep, JourneyStep lastStep) {
235+
JourneyStep step;
236+
237+
for (step = firstStep; step < lastStep; step = JourneyStep((int) step + 1)) {
238+
if (!session.journey.hasStep(step)) {
239+
continue;
240+
}
241+
242+
string stepString = journeyStepToStringLowerCase(step);
243+
string stepDir = session.responseDir + "/steps/" + stepString;
244+
session.stepDirFds.insert(make_pair(step, openDirFd(stepDir)));
245+
}
246+
}
247+
248+
int openDirFd(const string &path) {
249+
int fd = open(path.c_str(), O_RDONLY);
250+
if (fd == -1) {
251+
int e = errno;
252+
throw FileSystemException("Cannot open " + path, e, path);
253+
}
254+
return fd;
255+
}
256+
219257
void initializeResult() {
220258
session.result.initialize(*context, config);
221259
}
@@ -549,6 +587,7 @@ class HandshakePrepare {
549587

550588
resolveUserAndGroup();
551589
createWorkDir();
590+
openWorkDirSubdirFds();
552591
initializeResult();
553592

554593
UPDATE_TRACE_POINT();

src/agent/Core/SpawningKit/Handshake/Session.h

+34-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* Phusion Passenger - https://www.phusionpassenger.com/
3-
* Copyright (c) 2016-2017 Phusion Holding B.V.
3+
* Copyright (c) 2016-2018 Phusion Holding B.V.
44
*
55
* "Passenger", "Phusion Passenger" and "Union Station" are registered
66
* trademarks of Phusion Holding B.V.
@@ -28,6 +28,7 @@
2828

2929
#include <boost/scoped_ptr.hpp>
3030
#include <string>
31+
#include <map>
3132

3233
#include <Utils.h>
3334
#include <Core/SpawningKit/Context.h>
@@ -50,6 +51,12 @@ struct HandshakeSession {
5051
boost::scoped_ptr<HandshakeWorkDir> workDir;
5152
string responseDir;
5253
string envDumpDir;
54+
int workDirFd;
55+
int responseDirFd;
56+
int responseErrorDirFd;
57+
int envDumpDirFd;
58+
int envDumpAnnotationsDirFd;
59+
map<JourneyStep, int> stepDirFds;
5360
Journey journey;
5461
Result result;
5562

@@ -69,6 +76,11 @@ struct HandshakeSession {
6976
HandshakeSession(Context &_context, Config &_config, JourneyType journeyType)
7077
: context(&_context),
7178
config(&_config),
79+
workDirFd(-1),
80+
responseDirFd(-1),
81+
responseErrorDirFd(-1),
82+
envDumpDirFd(-1),
83+
envDumpAnnotationsDirFd(-1),
7284
journey(journeyType, !_config.genericApp && _config.startsUsingWrapper),
7385
uid(USER_NOT_GIVEN),
7486
gid(GROUP_NOT_GIVEN),
@@ -77,6 +89,27 @@ struct HandshakeSession {
7789
{ }
7890

7991
~HandshakeSession() {
92+
if (workDirFd != -1) {
93+
safelyClose(workDirFd, true);
94+
}
95+
if (responseDirFd != -1) {
96+
safelyClose(responseDirFd, true);
97+
}
98+
if (responseErrorDirFd != -1) {
99+
safelyClose(responseErrorDirFd, true);
100+
}
101+
if (envDumpDirFd != -1) {
102+
safelyClose(envDumpDirFd, true);
103+
}
104+
if (envDumpAnnotationsDirFd != -1) {
105+
safelyClose(envDumpAnnotationsDirFd, true);
106+
}
107+
108+
map<JourneyStep, int>::iterator it, end = stepDirFds.end();
109+
for (it = stepDirFds.begin(); it != end; it++) {
110+
safelyClose(it->second);
111+
}
112+
80113
if (config->debugWorkDir && workDir != NULL) {
81114
string path = workDir->dontRemoveOnDestruction();
82115
P_NOTICE("Work directory " << path << " preserved for debugging");

0 commit comments

Comments
 (0)