Changeset View
Standalone View
src/ioslaves/file/kauth/filehelper.cpp
Show All 15 Lines | 1 | /*** | |||
---|---|---|---|---|---|
16 | 16 | | |||
17 | You should have received a copy of the GNU Lesser General Public | 17 | You should have received a copy of the GNU Lesser General Public | ||
18 | License along with this library. If not, see <http://www.gnu.org/licenses/>. | 18 | License along with this library. If not, see <http://www.gnu.org/licenses/>. | ||
19 | ***/ | 19 | ***/ | ||
20 | 20 | | |||
21 | #include <utime.h> | 21 | #include <utime.h> | ||
22 | #include <fcntl.h> | 22 | #include <fcntl.h> | ||
23 | #include <unistd.h> | 23 | #include <unistd.h> | ||
24 | #include <dirent.h> | | |||
25 | #include <sys/stat.h> | | |||
26 | #include <errno.h> | 24 | #include <errno.h> | ||
25 | #include <libgen.h> | ||||
26 | #include <grp.h> | ||||
27 | #include <sys/stat.h> | ||||
28 | #include <sys/time.h> | ||||
27 | 29 | | |||
28 | #include "filehelper.h" | 30 | #include "filehelper.h" | ||
29 | #include "fdsender.h" | 31 | #include "fdsender.h" | ||
30 | #include "../file_p.h" | 32 | #include "../file_p.h" | ||
31 | 33 | | |||
34 | struct Privilege { | ||||
35 | uid_t uid; | ||||
36 | gid_t gid; | ||||
37 | }; | ||||
38 | | ||||
39 | static ActionType intToActionType(int action) | ||||
40 | { | ||||
41 | switch (action) { | ||||
42 | case 1: return CHMOD; | ||||
43 | case 2: return CHOWN; | ||||
44 | case 3: return DEL; | ||||
45 | case 4: return MKDIR; | ||||
46 | case 5: return OPEN; | ||||
47 | case 6: return OPENDIR; | ||||
48 | case 7: return RENAME; | ||||
49 | case 8: return RMDIR; | ||||
50 | case 9: return SYMLINK; | ||||
51 | case 10: return UTIME; | ||||
52 | default: return UNKNOWN; | ||||
53 | } | ||||
54 | } | ||||
55 | | ||||
32 | static bool sendFileDescriptor(int fd, const char *socketPath) | 56 | static bool sendFileDescriptor(int fd, const char *socketPath) | ||
33 | { | 57 | { | ||
34 | FdSender fdSender(socketPath); | 58 | FdSender fdSender(socketPath); | ||
35 | if (fdSender.isConnected() && fdSender.sendFileDescriptor(fd)) { | 59 | if (fdSender.isConnected() && fdSender.sendFileDescriptor(fd)) { | ||
36 | return true; | 60 | return true; | ||
37 | } | 61 | } | ||
38 | return false; | 62 | return false; | ||
39 | } | 63 | } | ||
40 | 64 | | |||
65 | static Privilege *getTargetPrivilege(int target_fd) | ||||
66 | { | ||||
67 | struct stat buf = {0}; | ||||
68 | if (fstat(target_fd, &buf) == -1) { | ||||
69 | return nullptr; | ||||
70 | } | ||||
71 | return new Privilege{buf.st_uid, buf.st_gid}; | ||||
72 | } | ||||
73 | | ||||
74 | static bool dropPrivilege(Privilege *p) | ||||
maltek: This is still subject to race conditions. You're opening the file with O_NOFOLLOW and calling… | |||||
75 | { | ||||
76 | if (!p) { | ||||
77 | return false; | ||||
78 | } | ||||
79 | | ||||
80 | uid_t newuid = p->uid; | ||||
81 | gid_t newgid = p->gid; | ||||
82 | | ||||
83 | //drop ancillary groups first because it requires root privileges. | ||||
84 | if (setgroups(1, &newgid) == -1) { | ||||
85 | return false; | ||||
86 | } | ||||
87 | //change effective gid and uid. | ||||
88 | if (setegid(newgid) == -1 || seteuid(newuid) == -1) { | ||||
89 | return false; | ||||
90 | } | ||||
91 | | ||||
92 | return true; | ||||
93 | } | ||||
94 | | ||||
95 | static void gainPrivilege(Privilege *p) | ||||
96 | { | ||||
97 | if (!p) { | ||||
98 | return; | ||||
99 | } | ||||
100 | | ||||
101 | uid_t olduid = p->uid; | ||||
102 | gid_t oldgid = p->gid; | ||||
103 | | ||||
104 | seteuid(olduid); | ||||
105 | setegid(oldgid); | ||||
After this return false, the process is in some undefined state with unknown groups, since there is no attempt to restore the original groups. The chance of these calls failing are quite slim, however - it can only really happen due to programming error when the program doesn't have privileges to call sete[ug]id. Personally, I'd just abort the program in such circumstances, but since it should never be happening, reasonable people might disagree. maltek: After this `return false`, the process is in some undefined state with unknown groups, since… | |||||
106 | setgroups(1, &oldgid); | ||||
107 | } | ||||
108 | | ||||
41 | ActionReply FileHelper::exec(const QVariantMap &args) | 109 | ActionReply FileHelper::exec(const QVariantMap &args) | ||
42 | { | 110 | { | ||
43 | ActionReply reply; | 111 | ActionReply reply; | ||
44 | QByteArray data = args["arguments"].toByteArray(); | 112 | QByteArray data = args["arguments"].toByteArray(); | ||
45 | QDataStream in(data); | 113 | QDataStream in(data); | ||
46 | int action; | 114 | int act; | ||
47 | QVariant arg1, arg2, arg3, arg4; | 115 | QVariant arg1, arg2, arg3, arg4; | ||
48 | in >> action >> arg1 >> arg2 >> arg3 >> arg4; | 116 | in >> act >> arg1 >> arg2 >> arg3 >> arg4; // act=action, arg1=source file, arg$n=dest file, mode, uid, gid, etc. | ||
117 | ActionType action = intToActionType(act); | ||||
49 | 118 | | |||
50 | // the path of an existing or a new file/dir upon which the method will operate | 119 | QByteArray tempPath1, tempPath2; | ||
51 | const QByteArray path = arg1.toByteArray(); | 120 | tempPath1 = tempPath2 = arg1.toByteArray(); | ||
121 | const QByteArray parentDir = dirname(tempPath1.data()); | ||||
122 | const QByteArray baseName = basename(tempPath2.data()); | ||||
123 | int parent_fd = open(parentDir.data(), O_DIRECTORY | O_PATH | O_NOFOLLOW); | ||||
maltek: This needs error handling. | |||||
124 | int base_fd = -1; | ||||
125 | | ||||
126 | Privilege *origPrivilege = new Privilege{geteuid(), getegid()}; | ||||
127 | Privilege *targetPrivilege = nullptr; | ||||
128 | | ||||
129 | if (action != CHMOD & action != CHOWN && action != UTIME) { | ||||
AT_SYMLINK_NOFOLLOW is not implemented for fchmodat, so this will always fail with ENOTSUP. It seems the only safe way to do this is to open() the file with O_NOFOLLOW, and then use fchmod(). maltek: AT_SYMLINK_NOFOLLOW is not implemented for fchmodat, so this will always fail with ENOTSUP. It… | |||||
typo: there's a second & missing after the first condition. (I don't think it ends up affecting the result.) maltek: typo: there's a second & missing after the first condition. (I don't think it ends up affecting… | |||||
130 | targetPrivilege = getTargetPrivilege(parent_fd); | ||||
131 | } else { | ||||
132 | base_fd = openat(parent_fd, baseName.data(), O_NOFOLLOW); | ||||
There's no error handling here, which will likely lead to weird EBADF errors getting returned later. maltek: There's no error handling here, which will likely lead to weird `EBADF` errors getting returned… | |||||
133 | targetPrivilege = getTargetPrivilege(base_fd); | ||||
For chown, dropping privileges here means that the chown later can't succeed - it's not possible to 'gift' a file to another user. I think it should be handled more like DEL/RMDIR/MKDIR etc. maltek: For `chown`, dropping privileges here means that the `chown` later can't succeed - it's not… | |||||
Ah! Since I was testing inside /opt I didn't notice. I think the order here should be: drop privilege -> change grp -> gain privilege -> change user. chinmoyr: Ah! Since I was testing inside /opt I didn't notice. I think the order here should be: drop… | |||||
IMO, it's fine (and less complicated) to just do both in one single privileged fchmod call. maltek: IMO, it's fine (and less complicated) to just do both in one single privileged `fchmod` call. | |||||
134 | } | ||||
52 | 135 | | |||
136 | if (dropPrivilege(targetPrivilege)) { | ||||
53 | switch(action) { | 137 | switch(action) { | ||
54 | case CHMOD: { | 138 | case CHMOD: { | ||
55 | int mode = arg2.toInt(); | 139 | int mode = arg2.toInt(); | ||
56 | if (chmod(path.data(), mode) == 0) { | 140 | if (fchmod(base_fd, mode) == -1) { | ||
57 | return reply; | 141 | reply.setError(errno); | ||
58 | } | 142 | } | ||
143 | close(base_fd); | ||||
59 | break; | 144 | break; | ||
60 | } | 145 | } | ||
146 | | ||||
61 | case CHOWN: { | 147 | case CHOWN: { | ||
62 | int uid = arg2.toInt(); | 148 | int uid = arg2.toInt(); | ||
63 | int gid = arg3.toInt(); | 149 | int gid = arg3.toInt(); | ||
64 | if (chown(path.data(), uid, gid) == 0) { | 150 | if (fchown(base_fd, uid, gid) == -1) { | ||
I just realized that this wouldn't allow changing the owner of symbolic links. The way to go here is lchown. maltek: I just realized that this wouldn't allow changing the owner of symbolic links. The way to go… | |||||
Do you think it'll be a bad idea to skip the case for symlinks in utime, chmod, chown, for now? Right now there's no code in KIO that requires these operations to be performed on the link itself. chinmoyr: Do you think it'll be a bad idea to skip the case for symlinks in utime, chmod, chown, for now? | |||||
Fine by me - I'm only really here to look for security problems, not to decide on which features are required for this to land. maltek: Fine by me - I'm only really here to look for security problems, not to decide on which… | |||||
65 | return reply; | 151 | reply.setError(errno); | ||
66 | } | 152 | } | ||
153 | close(base_fd); | ||||
67 | break; | 154 | break; | ||
68 | } | 155 | } | ||
69 | case DEL: { | 156 | | ||
70 | if (unlink(path.data()) == 0) { | 157 | case DEL: | ||
71 | return reply; | 158 | case RMDIR: { | ||
159 | int flags = 0; | ||||
160 | if (action == RMDIR) { | ||||
161 | flags |= AT_REMOVEDIR; | ||||
162 | } | ||||
163 | if (unlinkat(parent_fd, baseName.data(), flags) == -1) { | ||||
164 | reply.setError(errno); | ||||
72 | } | 165 | } | ||
73 | break; | 166 | break; | ||
74 | } | 167 | } | ||
168 | | ||||
75 | case MKDIR: { | 169 | case MKDIR: { | ||
76 | if (mkdir(path.data(), 0777) == 0) { | 170 | int mode = arg2.toInt(); | ||
77 | return reply; | 171 | if (mkdirat(parent_fd, baseName.data(), mode) == -1) { | ||
172 | reply.setError(errno); | ||||
78 | } | 173 | } | ||
79 | break; | 174 | break; | ||
80 | } | 175 | } | ||
81 | case OPEN: { | 176 | | ||
177 | case OPEN: | ||||
178 | case OPENDIR: { | ||||
82 | int oflags = arg2.toInt(); | 179 | int oflags = arg2.toInt(); | ||
83 | int mode = arg3.toInt(); | 180 | int mode = arg3.toInt(); | ||
84 | int fd = open(path.data(), oflags, mode); | 181 | int extraFlag = O_NOFOLLOW; | ||
85 | bool success = (fd != -1) && sendFileDescriptor(fd, arg4.toByteArray().constData()); | 182 | if (action == OPENDIR) { | ||
86 | close(fd); | 183 | extraFlag |= O_DIRECTORY; | ||
87 | if (success) { | 184 | } | ||
88 | return reply; | 185 | int fd = openat(parent_fd, baseName.data(), oflags | extraFlag, mode); | ||
89 | } | 186 | gainPrivilege(origPrivilege); | ||
90 | break; | 187 | bool sendSuccess = sendFileDescriptor(fd, arg4.toByteArray().constData()); | ||
In the error case, this attempts sending fd -1. I haven't checked the underlying code, but this will probably pollute errno with something unrelated to the underlying error. maltek: In the error case, this attempts sending fd `-1`. I haven't checked the underlying code, but… | |||||
91 | } | 188 | if (fd != -1 && sendSuccess) { | ||
92 | case OPENDIR: { | 189 | reply.setError(errno); | ||
93 | DIR *dp = opendir(path.data()); | | |||
94 | bool success = false; | | |||
95 | if (dp) { | | |||
96 | int fd = dirfd(dp); | | |||
97 | success = (fd != -1) && sendFileDescriptor(fd, arg4.toByteArray().constData()); | | |||
98 | closedir(dp); | | |||
99 | if (success) { | | |||
100 | return reply; | | |||
101 | } | | |||
102 | } | 190 | } | ||
103 | break; | 191 | break; | ||
104 | } | 192 | } | ||
193 | | ||||
105 | case RENAME: { | 194 | case RENAME: { | ||
106 | const QByteArray newName = arg2.toByteArray(); | 195 | tempPath1 = tempPath2 = arg2.toByteArray(); | ||
107 | if (rename(path.data(), newName.data()) == 0) { | 196 | const QByteArray newParentDir = dirname(tempPath1.data()); | ||
108 | return reply; | 197 | const QByteArray newBaseName = basename(tempPath2.data()); | ||
109 | } | 198 | int new_parent_fd = open(newParentDir.constData(), O_DIRECTORY | O_PATH | O_NOFOLLOW); | ||
110 | break; | 199 | if (renameat(parent_fd, baseName.data(), new_parent_fd, newBaseName.constData()) == -1) { | ||
111 | } | 200 | reply.setError(errno); | ||
112 | case RMDIR: { | | |||
113 | if (rmdir(path.data()) == 0) { | | |||
114 | return reply; | | |||
115 | } | 201 | } | ||
202 | close(new_parent_fd); | ||||
116 | break; | 203 | break; | ||
117 | } | 204 | } | ||
205 | | ||||
118 | case SYMLINK: { | 206 | case SYMLINK: { | ||
119 | const QByteArray target = arg2.toByteArray(); | 207 | const QByteArray target = arg2.toByteArray(); | ||
120 | if (symlink(target.data(), path.data()) == 0) { | 208 | if (symlinkat(target.data(), parent_fd, baseName.data()) == -1) { | ||
121 | return reply; | 209 | return reply; | ||
This early return skips all the deintialization code in the end of the function. Shouldn't it just be reply.setError(errno); like for all the other operations? maltek: This early return skips all the deintialization code in the end of the function. Shouldn't it… | |||||
122 | } | 210 | } | ||
123 | break; | 211 | break; | ||
124 | } | 212 | } | ||
213 | | ||||
125 | case UTIME: { | 214 | case UTIME: { | ||
126 | utimbuf ut; | 215 | timespec times[2]; | ||
127 | ut.actime = arg2.toULongLong(); | 216 | time_t actime = arg2.toULongLong(); | ||
128 | ut.modtime = arg3.toULongLong(); | 217 | time_t modtime = arg3.toULongLong(); | ||
129 | if (utime(path.data(), &ut) == 0) { | 218 | times[0].tv_sec = actime / 1000; | ||
130 | return reply; | 219 | times[0].tv_nsec = actime * 1000; | ||
220 | times[1].tv_sec = modtime / 1000; | ||||
221 | times[1].tv_nsec = modtime * 1000; | ||||
222 | if (futimens(base_fd, times) == -1) { | ||||
223 | reply.setError(errno); | ||||
131 | } | 224 | } | ||
225 | close(base_fd); | ||||
132 | break; | 226 | break; | ||
133 | } | 227 | } | ||
134 | }; | | |||
135 | 228 | | |||
136 | reply.setError(errno ? errno : -1); | 229 | default: break; | ||
230 | } | ||||
231 | gainPrivilege(origPrivilege); | ||||
232 | } | ||||
233 | if (origPrivilege) { | ||||
234 | delete origPrivilege; | ||||
235 | } | ||||
236 | if (targetPrivilege) { | ||||
237 | delete targetPrivilege; | ||||
238 | } | ||||
239 | close(parent_fd); | ||||
Somewhere here, I'd expect the privileges to be escalated again, to restore the state from before the call to dropPrivileges(). maltek: Somewhere here, I'd expect the privileges to be escalated again, to restore the state from… | |||||
137 | return reply; | 240 | return reply; | ||
138 | } | 241 | } | ||
139 | 242 | | |||
140 | KAUTH_HELPER_MAIN("org.kde.kio.file", FileHelper) | 243 | KAUTH_HELPER_MAIN("org.kde.kio.file", FileHelper) | ||
maltek: I *think* the divisions/multiplications are reversed here. |
This is still subject to race conditions. You're opening the file with O_NOFOLLOW and calling fstat on it, correctly. But then, you don't return this file descriptor to further work on it. So there's no way to know if the file checked here and the file that's changed later on are the same.
(It's also leaking the opened file descriptor, currently.)