OpenVPN
validate.c
Go to the documentation of this file.
1/*
2 * OpenVPN -- An application to securely tunnel IP networks
3 * over a single TCP/UDP port, with support for SSL/TLS-based
4 * session authentication and key exchange,
5 * packet encryption, packet authentication, and
6 * packet compression.
7 *
8 * Copyright (C) 2016-2024 Selva Nair <selva.nair@gmail.com>
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License along
20 * with this program; if not, write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 */
23
24#include "validate.h"
25
26#include <lmaccess.h>
27#include <shlwapi.h>
28#include <lm.h>
29
30static const WCHAR *white_list[] =
31{
32 L"auth-retry",
33 L"config",
34 L"log",
35 L"log-append",
36 L"management",
37 L"management-forget-disconnect",
38 L"management-hold",
39 L"management-query-passwords",
40 L"management-query-proxy",
41 L"management-signal",
42 L"management-up-down",
43 L"mute",
44 L"setenv",
45 L"service",
46 L"verb",
47 L"pull-filter",
48 L"script-security",
49
50 NULL /* last value */
51};
52
53static BOOL IsUserInGroup(PSID sid, const PTOKEN_GROUPS groups, const WCHAR *group_name);
54
55static PTOKEN_GROUPS GetTokenGroups(const HANDLE token);
56
57/*
58 * Check workdir\fname is inside config_dir
59 * The logic here is simple: we may reject some valid paths if ..\ is in any of the strings
60 */
61static BOOL
62CheckConfigPath(const WCHAR *workdir, const WCHAR *fname, const settings_t *s)
63{
64 WCHAR tmp[MAX_PATH];
65 const WCHAR *config_file = NULL;
66 const WCHAR *config_dir = NULL;
67
68 /* convert fname to full path */
69 if (PathIsRelativeW(fname) )
70 {
71 swprintf(tmp, _countof(tmp), L"%ls\\%ls", workdir, fname);
72 config_file = tmp;
73 }
74 else
75 {
76 config_file = fname;
77 }
78
79 config_dir = s->config_dir;
80
81 if (wcsncmp(config_dir, config_file, wcslen(config_dir)) == 0
82 && wcsstr(config_file + wcslen(config_dir), L"..") == NULL)
83 {
84 return TRUE;
85 }
86
87 return FALSE;
88}
89
90
91/*
92 * A simple linear search meant for a small wchar_t *array.
93 * Returns index to the item if found, -1 otherwise.
94 */
95static int
96OptionLookup(const WCHAR *name, const WCHAR *white_list[])
97{
98 int i;
99
100 for (i = 0; white_list[i]; i++)
101 {
102 if (wcscmp(white_list[i], name) == 0)
103 {
104 return i;
105 }
106 }
107
108 return -1;
109}
110
111/*
112 * The Administrators group may be localized or renamed by admins.
113 * Get the local name of the group using the SID.
114 */
115static BOOL
116GetBuiltinAdminGroupName(WCHAR *name, DWORD nlen)
117{
118 BOOL b = FALSE;
119 PSID admin_sid = NULL;
120 DWORD sid_size = SECURITY_MAX_SID_SIZE;
121 SID_NAME_USE snu;
122
123 WCHAR domain[MAX_NAME];
124 DWORD dlen = _countof(domain);
125
126 admin_sid = malloc(sid_size);
127 if (!admin_sid)
128 {
129 return FALSE;
130 }
131
132 b = CreateWellKnownSid(WinBuiltinAdministratorsSid, NULL, admin_sid, &sid_size);
133 if (b)
134 {
135 b = LookupAccountSidW(NULL, admin_sid, name, &nlen, domain, &dlen, &snu);
136 }
137
138 free(admin_sid);
139
140 return b;
141}
142
143BOOL
144IsAuthorizedUser(PSID sid, const HANDLE token, const WCHAR *ovpn_admin_group, const WCHAR *ovpn_service_user)
145{
146 const WCHAR *admin_group[2];
147 WCHAR username[MAX_NAME];
148 WCHAR domain[MAX_NAME];
149 WCHAR sysadmin_group[MAX_NAME];
150 DWORD len = MAX_NAME;
151 BOOL ret = FALSE;
152 SID_NAME_USE sid_type;
153
154 /* Get username */
155 if (!LookupAccountSidW(NULL, sid, username, &len, domain, &len, &sid_type))
156 {
157 MsgToEventLog(M_SYSERR, L"LookupAccountSid");
158 /* not fatal as this is now used only for logging */
159 username[0] = '\0';
160 domain[0] = '\0';
161 }
162
163 /* is this service account? */
164 if ((wcscmp(username, ovpn_service_user) == 0) && (wcscmp(domain, L"NT SERVICE") == 0))
165 {
166 return TRUE;
167 }
168
169 if (GetBuiltinAdminGroupName(sysadmin_group, _countof(sysadmin_group)))
170 {
171 admin_group[0] = sysadmin_group;
172 }
173 else
174 {
175 MsgToEventLog(M_SYSERR, L"Failed to get the name of Administrators group. Using the default.");
176 /* use the default value */
177 admin_group[0] = SYSTEM_ADMIN_GROUP;
178 }
179 admin_group[1] = ovpn_admin_group;
180
181 PTOKEN_GROUPS token_groups = GetTokenGroups(token);
182 for (int i = 0; i < 2; ++i)
183 {
184 ret = IsUserInGroup(sid, token_groups, admin_group[i]);
185 if (ret)
186 {
187 MsgToEventLog(M_INFO, L"Authorizing user '%ls@%ls' by virtue of membership in group '%ls'",
188 username, domain, admin_group[i]);
189 goto out;
190 }
191 }
192
193out:
194 free(token_groups);
195 return ret;
196}
197
203static PTOKEN_GROUPS
204GetTokenGroups(const HANDLE token)
205{
206 PTOKEN_GROUPS groups = NULL;
207 DWORD buf_size = 0;
208
209 if (!GetTokenInformation(token, TokenGroups, groups, buf_size, &buf_size)
210 && GetLastError() == ERROR_INSUFFICIENT_BUFFER)
211 {
212 groups = malloc(buf_size);
213 }
214 if (!groups)
215 {
216 MsgToEventLog(M_SYSERR, L"GetTokenGroups");
217 }
218 else if (!GetTokenInformation(token, TokenGroups, groups, buf_size, &buf_size))
219 {
220 MsgToEventLog(M_SYSERR, L"GetTokenInformation");
221 free(groups);
222 }
223 return groups;
224}
225
226/*
227 * Find SID from name
228 *
229 * On input sid buffer should have space for at least sid_size bytes.
230 * Returns true on success, false on failure.
231 * Suggest: in caller allocate sid to hold SECURITY_MAX_SID_SIZE bytes
232 */
233static BOOL
234LookupSID(const WCHAR *name, PSID sid, DWORD sid_size)
235{
236 SID_NAME_USE su;
237 WCHAR domain[MAX_NAME];
238 DWORD dlen = _countof(domain);
239
240 if (!LookupAccountName(NULL, name, sid, &sid_size, domain, &dlen, &su))
241 {
242 return FALSE; /* not fatal as the group may not exist */
243 }
244 return TRUE;
245}
246
259static BOOL
260IsUserInGroup(PSID sid, const PTOKEN_GROUPS token_groups, const WCHAR *group_name)
261{
262 BOOL ret = FALSE;
263 DWORD_PTR resume = 0;
264 DWORD err;
265 BYTE grp_sid[SECURITY_MAX_SID_SIZE];
266 int nloop = 0; /* a counter used to not get stuck in the do .. while() */
267
268 /* first check in the token groups */
269 if (token_groups && LookupSID(group_name, (PSID) grp_sid, _countof(grp_sid)))
270 {
271 for (DWORD i = 0; i < token_groups->GroupCount; ++i)
272 {
273 if (EqualSid((PSID) grp_sid, token_groups->Groups[i].Sid))
274 {
275 return TRUE;
276 }
277 }
278 }
279
280 /* check user's SID is a member of the group */
281 if (!sid)
282 {
283 return FALSE;
284 }
285 do
286 {
287 DWORD nread, nmax;
288 LOCALGROUP_MEMBERS_INFO_0 *members = NULL;
289 err = NetLocalGroupGetMembers(NULL, group_name, 0, (LPBYTE *) &members,
290 MAX_PREFERRED_LENGTH, &nread, &nmax, &resume);
291 if ((err != NERR_Success && err != ERROR_MORE_DATA))
292 {
293 break;
294 }
295 /* If a match is already found, ret == TRUE and the loop is skipped */
296 for (DWORD i = 0; i < nread && !ret; ++i)
297 {
298 ret = EqualSid(members[i].lgrmi0_sid, sid);
299 }
300 NetApiBufferFree(members);
301 /* MSDN says the lookup should always iterate until err != ERROR_MORE_DATA */
302 } while (err == ERROR_MORE_DATA && nloop++ < 100);
303
304 if (err != NERR_Success && err != NERR_GroupNotFound)
305 {
306 SetLastError(err);
307 MsgToEventLog(M_SYSERR, L"In NetLocalGroupGetMembers for group '%ls'", group_name);
308 }
309
310 return ret;
311}
312
313/*
314 * Check whether option argv[0] is white-listed. If argv[0] == "--config",
315 * also check that argv[1], if present, passes CheckConfigPath().
316 * The caller should set argc to the number of valid elements in argv[] array.
317 */
318BOOL
319CheckOption(const WCHAR *workdir, int argc, WCHAR *argv[], const settings_t *s)
320{
321 /* Do not modify argv or *argv -- ideally it should be const WCHAR *const *, but alas...*/
322
323 if (wcscmp(argv[0], L"--config") == 0
324 && argc > 1
325 && !CheckConfigPath(workdir, argv[1], s)
326 )
327 {
328 return FALSE;
329 }
330
331 /* option name starts at 2 characters from argv[i] */
332 if (OptionLookup(argv[0] + 2, white_list) == -1) /* not found */
333 {
334 return FALSE;
335 }
336
337 return TRUE;
338}
DWORD MsgToEventLog(DWORD flags, LPCWSTR format,...)
Definition common.c:231
#define M_INFO
Definition errlevel.h:55
#define M_SYSERR
Definition service.h:46
#define MAX_NAME
Definition service.h:62
Definition argv.h:35
WCHAR config_dir[MAX_PATH]
Definition service.h:65
static BOOL CheckConfigPath(const WCHAR *workdir, const WCHAR *fname, const settings_t *s)
Definition validate.c:62
static const WCHAR * white_list[]
Definition validate.c:30
static int OptionLookup(const WCHAR *name, const WCHAR *white_list[])
Definition validate.c:96
BOOL IsAuthorizedUser(PSID sid, const HANDLE token, const WCHAR *ovpn_admin_group, const WCHAR *ovpn_service_user)
Definition validate.c:144
static BOOL IsUserInGroup(PSID sid, const PTOKEN_GROUPS groups, const WCHAR *group_name)
User is in group if the token groups contain the SID of the group of if the user is a direct member o...
Definition validate.c:260
static BOOL LookupSID(const WCHAR *name, PSID sid, DWORD sid_size)
Definition validate.c:234
BOOL CheckOption(const WCHAR *workdir, int argc, WCHAR *argv[], const settings_t *s)
Definition validate.c:319
static BOOL GetBuiltinAdminGroupName(WCHAR *name, DWORD nlen)
Definition validate.c:116
static PTOKEN_GROUPS GetTokenGroups(const HANDLE token)
Get a list of groups in token.
Definition validate.c:204
#define SYSTEM_ADMIN_GROUP
Definition validate.h:31