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