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