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-2025 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, see <https://www.gnu.org/licenses/>.
21 */
22
23#include "validate.h"
24
25#include <lmaccess.h>
26#include <shlwapi.h>
27#include <lm.h>
28#include <pathcch.h>
29
30#ifndef HAVE_PATHCCH_ENSURE_TRAILING_SLASH
31#define PATHCCH_ENSURE_TRAILING_SLASH 0x20
32#endif
33
34static const WCHAR *white_list[] = {
35 L"auth-retry",
36 L"config",
37 L"log",
38 L"log-append",
39 L"management",
40 L"management-forget-disconnect",
41 L"management-hold",
42 L"management-query-passwords",
43 L"management-query-proxy",
44 L"management-signal",
45 L"management-up-down",
46 L"mute",
47 L"setenv",
48 L"service",
49 L"verb",
50 L"pull-filter",
51 L"script-security",
52
53 NULL /* last value */
54};
55
56static BOOL IsUserInGroup(PSID sid, const PTOKEN_GROUPS groups, const WCHAR *group_name);
57
58static PTOKEN_GROUPS GetTokenGroups(const HANDLE token);
59
60/*
61 * Check workdir\fname is inside config_dir
62 * The logic here is simple: we may reject some valid paths if ..\ is in any of the strings
63 */
64static BOOL
65CheckConfigPath(const WCHAR *workdir, const WCHAR *fname, const settings_t *s)
66{
67 WCHAR tmp[MAX_PATH];
68 const WCHAR *config_file = NULL;
69 WCHAR config_dir[MAX_PATH];
70
71 /* fname = stdin is special: do not treat it as a relative path */
72 if (wcscmp(fname, L"stdin") == 0)
73 {
74 return FALSE;
75 }
76 /* convert fname to full path */
77 if (PathIsRelativeW(fname))
78 {
79 swprintf(tmp, _countof(tmp), L"%ls\\%ls", workdir, fname);
80 config_file = tmp;
81 }
82 else
83 {
84 config_file = fname;
85 }
86
87 /* canonicalize config_dir and add trailing slash before comparison */
88 HRESULT res = PathCchCanonicalizeEx(config_dir, _countof(config_dir), s->config_dir,
90
91 if (res == S_OK
92 && wcsncmp(config_dir, config_file, wcslen(config_dir)) == 0
93 && wcsstr(config_file + wcslen(config_dir), L"..") == NULL)
94 {
95 return TRUE;
96 }
97
98 return FALSE;
99}
100
101
102/*
103 * A simple linear search meant for a small wchar_t *array.
104 * Returns index to the item if found, -1 otherwise.
105 */
106static int
107OptionLookup(const WCHAR *name, const WCHAR *white_list[])
108{
109 int i;
110
111 for (i = 0; white_list[i]; i++)
112 {
113 if (wcscmp(white_list[i], name) == 0)
114 {
115 return i;
116 }
117 }
118
119 return -1;
120}
121
122/*
123 * The Administrators group may be localized or renamed by admins.
124 * Get the local name of the group using the SID.
125 */
126static BOOL
127GetBuiltinAdminGroupName(WCHAR *name, DWORD nlen)
128{
129 BOOL b = FALSE;
130 PSID admin_sid = NULL;
131 DWORD sid_size = SECURITY_MAX_SID_SIZE;
132 SID_NAME_USE snu;
133
134 WCHAR domain[MAX_NAME];
135 DWORD dlen = _countof(domain);
136
137 admin_sid = malloc(sid_size);
138 if (!admin_sid)
139 {
140 return FALSE;
141 }
142
143 b = CreateWellKnownSid(WinBuiltinAdministratorsSid, NULL, admin_sid, &sid_size);
144 if (b)
145 {
146 b = LookupAccountSidW(NULL, admin_sid, name, &nlen, domain, &dlen, &snu);
147 }
148
149 free(admin_sid);
150
151 return b;
152}
153
154BOOL
155IsAuthorizedUser(PSID sid, const HANDLE token, const WCHAR *ovpn_admin_group,
156 const WCHAR *ovpn_service_user)
157{
158 const WCHAR *admin_group[2];
159 WCHAR username[MAX_NAME];
160 WCHAR domain[MAX_NAME];
161 WCHAR sysadmin_group[MAX_NAME];
162 DWORD len = MAX_NAME;
163 BOOL ret = FALSE;
164 SID_NAME_USE sid_type;
165
166 /* Get username */
167 if (!LookupAccountSidW(NULL, sid, username, &len, domain, &len, &sid_type))
168 {
169 MsgToEventLog(M_SYSERR, L"LookupAccountSid");
170 /* not fatal as this is now used only for logging */
171 username[0] = '\0';
172 domain[0] = '\0';
173 }
174
175 /* is this service account? */
176 if ((wcscmp(username, ovpn_service_user) == 0) && (wcscmp(domain, L"NT SERVICE") == 0))
177 {
178 return TRUE;
179 }
180
181 if (GetBuiltinAdminGroupName(sysadmin_group, _countof(sysadmin_group)))
182 {
183 admin_group[0] = sysadmin_group;
184 }
185 else
186 {
188 L"Failed to get the name of Administrators group. Using the default.");
189 /* use the default value */
190 admin_group[0] = SYSTEM_ADMIN_GROUP;
191 }
192 admin_group[1] = ovpn_admin_group;
193
194 PTOKEN_GROUPS token_groups = GetTokenGroups(token);
195 for (int i = 0; i < 2; ++i)
196 {
197 ret = IsUserInGroup(sid, token_groups, admin_group[i]);
198 if (ret)
199 {
201 L"Authorizing user '%ls@%ls' by virtue of membership in group '%ls'",
202 username, domain, admin_group[i]);
203 goto out;
204 }
205 }
206
207out:
208 free(token_groups);
209 return ret;
210}
211
217static PTOKEN_GROUPS
218GetTokenGroups(const HANDLE token)
219{
220 PTOKEN_GROUPS groups = NULL;
221 DWORD buf_size = 0;
222
223 if (!GetTokenInformation(token, TokenGroups, groups, buf_size, &buf_size)
224 && GetLastError() == ERROR_INSUFFICIENT_BUFFER)
225 {
226 groups = malloc(buf_size);
227 }
228 if (!groups)
229 {
230 MsgToEventLog(M_SYSERR, L"GetTokenGroups");
231 }
232 else if (!GetTokenInformation(token, TokenGroups, groups, buf_size, &buf_size))
233 {
234 MsgToEventLog(M_SYSERR, L"GetTokenInformation");
235 free(groups);
236 }
237 return groups;
238}
239
240/*
241 * Find SID from name
242 *
243 * On input sid buffer should have space for at least sid_size bytes.
244 * Returns true on success, false on failure.
245 * Suggest: in caller allocate sid to hold SECURITY_MAX_SID_SIZE bytes
246 */
247static BOOL
248LookupSID(const WCHAR *name, PSID sid, DWORD sid_size)
249{
250 SID_NAME_USE su;
251 WCHAR domain[MAX_NAME];
252 DWORD dlen = _countof(domain);
253
254 if (!LookupAccountName(NULL, name, sid, &sid_size, domain, &dlen, &su))
255 {
256 return FALSE; /* not fatal as the group may not exist */
257 }
258 return TRUE;
259}
260
273static BOOL
274IsUserInGroup(PSID sid, const PTOKEN_GROUPS token_groups, const WCHAR *group_name)
275{
276 BOOL ret = FALSE;
277 DWORD_PTR resume = 0;
278 DWORD err;
279 BYTE grp_sid[SECURITY_MAX_SID_SIZE];
280 int nloop = 0; /* a counter used to not get stuck in the do .. while() */
281
282 /* first check in the token groups */
283 if (token_groups && LookupSID(group_name, (PSID)grp_sid, _countof(grp_sid)))
284 {
285 for (DWORD i = 0; i < token_groups->GroupCount; ++i)
286 {
287 if (EqualSid((PSID)grp_sid, token_groups->Groups[i].Sid))
288 {
289 return TRUE;
290 }
291 }
292 }
293
294 /* check user's SID is a member of the group */
295 if (!sid)
296 {
297 return FALSE;
298 }
299 do
300 {
301 DWORD nread, nmax;
302 LOCALGROUP_MEMBERS_INFO_0 *members = NULL;
303 err = NetLocalGroupGetMembers(NULL, group_name, 0, (LPBYTE *)&members, MAX_PREFERRED_LENGTH,
304 &nread, &nmax, &resume);
305 if ((err != NERR_Success && err != ERROR_MORE_DATA))
306 {
307 break;
308 }
309 /* If a match is already found, ret == TRUE and the loop is skipped */
310 for (DWORD i = 0; i < nread && !ret; ++i)
311 {
312 ret = EqualSid(members[i].lgrmi0_sid, sid);
313 }
314 NetApiBufferFree(members);
315 /* MSDN says the lookup should always iterate until err != ERROR_MORE_DATA */
316 } while (err == ERROR_MORE_DATA && nloop++ < 100);
317
318 if (err != NERR_Success && err != NERR_GroupNotFound)
319 {
320 SetLastError(err);
321 MsgToEventLog(M_SYSERR, L"In NetLocalGroupGetMembers for group '%ls'", group_name);
322 }
323
324 return ret;
325}
326
327/*
328 * Check whether option argv[0] is white-listed. If argv[0] == "--config",
329 * also check that argv[1], if present, passes CheckConfigPath().
330 * The caller should set argc to the number of valid elements in argv[] array.
331 */
332BOOL
333CheckOption(const WCHAR *workdir, int argc, WCHAR *argv[], const settings_t *s)
334{
335 /* Do not modify argv or *argv -- ideally it should be const WCHAR *const *, but alas...*/
336
337 if (wcscmp(argv[0], L"--config") == 0 && argc > 1 && !CheckConfigPath(workdir, argv[1], s))
338 {
339 return FALSE;
340 }
341
342 /* option name starts at 2 characters from argv[i] */
343 if (OptionLookup(argv[0] + 2, white_list) == -1) /* not found */
344 {
345 return FALSE;
346 }
347
348 return TRUE;
349}
DWORD MsgToEventLog(DWORD flags, LPCWSTR format,...)
Definition common.c:235
#define M_INFO
Definition errlevel.h:54
#define M_SYSERR
Definition service.h:45
#define MAX_NAME
Definition service.h:63
Definition argv.h:35
WCHAR config_dir[MAX_PATH]
Definition service.h:67
static BOOL CheckConfigPath(const WCHAR *workdir, const WCHAR *fname, const settings_t *s)
Definition validate.c:65
static const WCHAR * white_list[]
Definition validate.c:34
#define PATHCCH_ENSURE_TRAILING_SLASH
Definition validate.c:31
static int OptionLookup(const WCHAR *name, const WCHAR *white_list[])
Definition validate.c:107
BOOL IsAuthorizedUser(PSID sid, const HANDLE token, const WCHAR *ovpn_admin_group, const WCHAR *ovpn_service_user)
Definition validate.c:155
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:274
static BOOL LookupSID(const WCHAR *name, PSID sid, DWORD sid_size)
Definition validate.c:248
BOOL CheckOption(const WCHAR *workdir, int argc, WCHAR *argv[], const settings_t *s)
Definition validate.c:333
static BOOL GetBuiltinAdminGroupName(WCHAR *name, DWORD nlen)
Definition validate.c:127
static PTOKEN_GROUPS GetTokenGroups(const HANDLE token)
Get a list of groups in token.
Definition validate.c:218
#define SYSTEM_ADMIN_GROUP
Definition validate.h:30