Browse code

Restrict options/configs for startup through interactive service

Windows only:

- Allow only a set of whitelisted options in the command line options
passed by
interactive service clients unless
(i) user is the local Adminsitrator group
AND/OR
(ii) in a predefined group (see below)
Only the group membership is checked, the client process need not be
running with
any elevated privileges available to those groups.

- Restrict config files to config_dir or it sub directories unless (i)
and/or (ii) above
is true (config_dir is as defined in HKLM\Software\OpenVPN\config_dir)

- The predefined group may be set in the registry
HKLM\Software\OpenVPN\ovpn_admin_group
(default: "OpenVPN Administrators")

- The white-list of options is a simple flat array of option strings
(without leading --)
defined in validate.c

- Further options may be added to the whitelist without breaking the GUI
-- the startup
data is passed from the GUI to the service the same way as before.

Notes to GUI developers:
(i) If the user is an administrator, the service will grant all privileges
even if
the GUI is not running elevated. This is practically equivalent to
'highestAvailable' without the risks of running the GUI elevated.

(ii) If the option checks fail, openvpn is not started, but an error
message
is passed back to the service pipe and written to event log. Currently the
GUI does
not read from the service pipe -- this needs fixing.

v2 changes:
- checked non-unicode build and fixed an error -- in case anyone builds
non-unicode
- added an info message to event log when user auth succeeds

Signed-off-by: Selva Nair <selva.nair@gmail.com>
Acked-by: Gert Doering <gert@greenie.muc.de>
Message-Id: <1455937988-12414-1-git-send-email-selva.nair@gmail.com>
URL: http://article.gmane.org/gmane.network.openvpn.devel/11225
Signed-off-by: Gert Doering <gert@greenie.muc.de>

Selva Nair authored on 2016/02/20 12:13:08
Showing 6 changed files
... ...
@@ -26,7 +26,7 @@ openvpnserv_CFLAGS = \
26 26
 	-municode -D_UNICODE \
27 27
 	-UNTDDI_VERSION -U_WIN32_WINNT \
28 28
 	-D_WIN32_WINNT=_WIN32_WINNT_VISTA
29
-openvpnserv_LDADD = -ladvapi32 -luserenv -liphlpapi -lws2_32
29
+openvpnserv_LDADD = -ladvapi32 -luserenv -liphlpapi -lshlwapi -lnetapi32 -lws2_32
30 30
 endif
31 31
 
32 32
 openvpnserv_SOURCES = \
... ...
@@ -34,4 +34,5 @@ openvpnserv_SOURCES = \
34 34
 	automatic.c \
35 35
 	interactive.c \
36 36
 	service.c service.h \
37
+        validate.c validate.h \
37 38
 	openvpnserv_resources.rc
... ...
@@ -23,7 +23,7 @@
23 23
  */
24 24
 
25 25
 #include <service.h>
26
-
26
+#include <validate.h>
27 27
 /*
28 28
  * These are necessary due to certain buggy implementations of (v)snprintf,
29 29
  * that don't guarantee null termination for size > 0.
... ...
@@ -53,7 +53,6 @@ openvpn_sntprintf (LPTSTR str, size_t size, LPCTSTR format, ...)
53 53
   return len;
54 54
 }
55 55
 
56
-
57 56
 #define REG_KEY  TEXT("SOFTWARE\\" PACKAGE_NAME)
58 57
 
59 58
 static DWORD
... ...
@@ -114,6 +113,13 @@ GetOpenvpnSettings (settings_t *s)
114 114
   if (error != ERROR_SUCCESS)
115 115
     goto out;
116 116
 
117
+  /* read if present, else use default */
118
+  error = GetRegString (key, TEXT("ovpn_admin_group"), s->ovpn_admin_group, sizeof (s->ovpn_admin_group));
119
+  if (error != ERROR_SUCCESS)
120
+  {
121
+    openvpn_sntprintf(s->ovpn_admin_group, _countof(s->ovpn_admin_group), OVPN_ADMIN_GROUP);
122
+    error = 0; /* this error is not fatal */
123
+  }
117 124
   /* set process priority */
118 125
   if (!_tcsicmp (priority, TEXT("IDLE_PRIORITY_CLASS")))
119 126
     s->priority = IDLE_PRIORITY_CLASS;
... ...
@@ -194,7 +200,8 @@ MsgToEventLog (DWORD flags, LPCTSTR format, ...)
194 194
   if (hEventSource != NULL)
195 195
     {
196 196
       openvpn_sntprintf (msg[0], _countof (msg[0]),
197
-                         TEXT("%s error: %s"), APPNAME, err_msg);
197
+                         TEXT("%s%s: %s"), APPNAME,
198
+                         (flags & MSG_FLAGS_ERROR) ? TEXT(" error") : TEXT(""), err_msg);
198 199
 
199 200
       va_start (arglist, format);
200 201
       openvpn_vsntprintf (msg[1], _countof (msg[1]), format, arglist);
... ...
@@ -33,8 +33,10 @@
33 33
 #include <aclapi.h>
34 34
 #include <stdio.h>
35 35
 #include <sddl.h>
36
+#include <shellapi.h>
36 37
 
37 38
 #include "openvpn-msg.h"
39
+#include "validate.h"
38 40
 
39 41
 #define IO_TIMEOUT  2000 /*ms*/
40 42
 
... ...
@@ -292,6 +294,93 @@ ReturnOpenvpnOutput (HANDLE pipe, HANDLE ovpn_output, DWORD count, LPHANDLE even
292 292
   free (wide_output);
293 293
 }
294 294
 
295
+/*
296
+ * Validate options against a white list. Also check the config_file is
297
+ * inside the config_dir. The white list is defined in validate.c
298
+ * Returns true on success
299
+ */
300
+static BOOL
301
+ValidateOptions (HANDLE pipe, const WCHAR *workdir, const WCHAR *options)
302
+{
303
+    WCHAR **argv;
304
+    int argc;
305
+    WCHAR buf[256];
306
+    BOOL ret = FALSE;
307
+    int i;
308
+    const WCHAR *msg1 = L"You have specified a config file location (%s relative to %s)"
309
+                        " that requires admin approval. This error may be avoided"
310
+                        " by adding your account to the \"%s\" group";
311
+
312
+    const WCHAR *msg2 = L"You have specified an option (%s) that may be used"
313
+                         " only with admin approval. This error may be avoided"
314
+                         " by adding your account to the \"%s\" group";
315
+
316
+    argv = CommandLineToArgvW (options, &argc);
317
+
318
+    if (!argv)
319
+    {
320
+        ReturnLastError (pipe, L"CommandLineToArgvW");
321
+        ReturnError (pipe, ERROR_STARTUP_DATA, L"Cannot validate options", 1, &exit_event);
322
+        goto out;
323
+    }
324
+
325
+    /* Note: argv[0] is the first option */
326
+    if (argc < 1)  /* no options */
327
+    {
328
+        ret = TRUE;
329
+        goto out;
330
+    }
331
+
332
+    /*
333
+     * If only one argument, it is the config file
334
+     */
335
+    if (argc == 1)
336
+    {
337
+        WCHAR *argv_tmp[2] = { L"--config", argv[0] };
338
+
339
+        if (!CheckOption (workdir, 2, argv_tmp, &settings))
340
+        {
341
+            snwprintf (buf, _countof(buf), msg1, argv[0], workdir,
342
+                       settings.ovpn_admin_group);
343
+            buf[_countof(buf) - 1] = L'\0';
344
+            ReturnError (pipe, ERROR_STARTUP_DATA, buf, 1, &exit_event);
345
+        }
346
+        goto out;
347
+    }
348
+
349
+    for (i = 0; i < argc; ++i)
350
+    {
351
+        if (!IsOption(argv[i]))
352
+            continue;
353
+
354
+        if (!CheckOption (workdir, argc-i, &argv[i], &settings))
355
+        {
356
+            if (wcscmp(L"--config", argv[i]) == 0 && argc-i > 1)
357
+            {
358
+                snwprintf (buf, _countof(buf), msg1, argv[i+1], workdir,
359
+                            settings.ovpn_admin_group);
360
+                buf[_countof(buf) - 1] = L'\0';
361
+                ReturnError (pipe, ERROR_STARTUP_DATA, buf, 1, &exit_event);
362
+            }
363
+            else
364
+            {
365
+                snwprintf (buf, _countof(buf), msg2, argv[i],
366
+                           settings.ovpn_admin_group);
367
+                buf[_countof(buf) - 1] = L'\0';
368
+                ReturnError (pipe, ERROR_STARTUP_DATA, buf, 1, &exit_event);
369
+            }
370
+            goto out;
371
+        }
372
+    }
373
+
374
+    /* all options passed */
375
+    ret = TRUE;
376
+
377
+out:
378
+    if (argv)
379
+        LocalFree (argv);
380
+    return ret;
381
+}
295 382
 
296 383
 static BOOL
297 384
 GetStartupData (HANDLE pipe, STARTUP_DATA *sud)
... ...
@@ -835,6 +924,13 @@ RunOpenvpn (LPVOID p)
835 835
       goto out;
836 836
     }
837 837
 
838
+  /* Check user is authorized or options are white-listed */
839
+  if (!IsAuthorizedUser (ovpn_user->User.Sid, &settings) &&
840
+      !ValidateOptions (pipe, sud.directory, sud.options))
841
+    {
842
+      goto out;
843
+    }
844
+
838 845
   /* OpenVPN process DACL entry for access by service and user */
839 846
   ea[0].grfAccessPermissions = SPECIFIC_RIGHTS_ALL | STANDARD_RIGHTS_ALL;
840 847
   ea[0].grfAccessMode = SET_ACCESS;
... ...
@@ -61,11 +61,13 @@ typedef struct {
61 61
   DWORD start_type;
62 62
 } openvpn_service_t;
63 63
 
64
+#define MAX_NAME 256
64 65
 typedef struct {
65 66
   TCHAR exe_path[MAX_PATH];
66 67
   TCHAR config_dir[MAX_PATH];
67 68
   TCHAR ext_string[16];
68 69
   TCHAR log_dir[MAX_PATH];
70
+  TCHAR ovpn_admin_group[MAX_NAME];
69 71
   DWORD priority;
70 72
   BOOL append;
71 73
 } settings_t;
72 74
new file mode 100644
... ...
@@ -0,0 +1,208 @@
0
+/*
1
+ *  OpenVPN -- An application to securely tunnel IP networks
2
+ *             over a single TCP/UDP port, with support for SSL/TLS-based
3
+ *             session authentication and key exchange,
4
+ *             packet encryption, packet authentication, and
5
+ *             packet compression.
6
+ *
7
+ *  Copyright (C) 2016 Selva Nair <selva.nair@gmail.com>
8
+ *
9
+ *  This program is free software; you can redistribute it and/or modify
10
+ *  it under the terms of the GNU General Public License version 2
11
+ *  as published by the Free Software Foundation.
12
+ *
13
+ *  This program is distributed in the hope that it will be useful,
14
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
+ *  GNU General Public License for more details.
17
+ *
18
+ *  You should have received a copy of the GNU General Public License
19
+ *  along with this program (see the file COPYING included with this
20
+ *  distribution); if not, write to the Free Software Foundation, Inc.,
21
+ *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
22
+ */
23
+
24
+#include "validate.h"
25
+
26
+#include <lmaccess.h>
27
+
28
+static const WCHAR *white_list[] =
29
+    {
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
+
46
+       NULL                             /* last value */
47
+    };
48
+
49
+/*
50
+ * Check workdir\fname is inside config_dir
51
+ * The logic here is simple: we may reject some valid paths if ..\ is in any of the strings
52
+ */
53
+static BOOL
54
+CheckConfigPath (const WCHAR *workdir, const WCHAR *fname, const settings_t *s)
55
+{
56
+    WCHAR tmp[MAX_PATH];
57
+    WCHAR widepath[MAX_PATH];
58
+    WCHAR relpath[MAX_PATH];
59
+    const WCHAR *config_file = NULL;
60
+    const WCHAR *config_dir = NULL;
61
+
62
+    /* convert fname to full path */
63
+    if (PathIsRelativeW (fname) )
64
+    {
65
+        snwprintf (tmp, _countof(tmp), L"%s\\%s", workdir, fname);
66
+        tmp[_countof(tmp)-1] = L'\0';
67
+        config_file = tmp;
68
+    }
69
+    else
70
+    {
71
+        config_file = fname;
72
+    }
73
+
74
+#ifdef UNICODE
75
+    config_dir = s->config_dir;
76
+#else
77
+    if (MultiByteToWideChar (CP_UTF8, 0, s->config_dir, -1, widepath, MAX_PATH) == 0)
78
+    {
79
+        MsgToEventLog (M_SYSERR, TEXT("Failed to convert config_dir name to WideChar"));
80
+        return FALSE;
81
+    }
82
+    config_dir = widepath;
83
+#endif
84
+
85
+    if (wcsncmp (config_dir, config_file, wcslen(config_dir)) == 0 &&
86
+        wcsstr (config_file + wcslen(config_dir), L"..") == NULL )
87
+        return TRUE;
88
+
89
+    return FALSE;
90
+}
91
+
92
+
93
+/*
94
+ * A simple linear search meant for a small wchar_t *array.
95
+ * Returns index to the item if found, -1 otherwise.
96
+ */
97
+static int
98
+OptionLookup (const WCHAR *name, const WCHAR *white_list[])
99
+{
100
+    int i;
101
+
102
+    for (i = 0 ; white_list[i]; i++)
103
+    {
104
+        if ( wcscmp(white_list[i], name) == 0 )
105
+            return i;
106
+    }
107
+
108
+    return -1;
109
+}
110
+
111
+/*
112
+ * Check whether user is a member of Administrators group or
113
+ * the group specified in s->ovpn_admin_group
114
+ */
115
+BOOL
116
+IsAuthorizedUser (SID *sid, settings_t *s)
117
+{
118
+    LOCALGROUP_USERS_INFO_0 *groups = NULL;
119
+    DWORD nread;
120
+    DWORD nmax;
121
+    WCHAR *tmp = NULL;
122
+    const WCHAR *admin_group[2];
123
+    WCHAR username[MAX_NAME];
124
+    WCHAR domain[MAX_NAME];
125
+    DWORD err, len = MAX_NAME;
126
+    int i;
127
+    BOOL ret = FALSE;
128
+    SID_NAME_USE sid_type;
129
+
130
+    /* Get username */
131
+    if (!LookupAccountSidW (NULL, sid, username, &len, domain, &len, &sid_type))
132
+    {
133
+        MsgToEventLog (M_SYSERR, TEXT("LookupAccountSid"));
134
+        goto out;
135
+    }
136
+
137
+    /* Get an array of groups the user is member of */
138
+    err = NetUserGetLocalGroups (NULL, username, 0, LG_INCLUDE_INDIRECT, (LPBYTE *) &groups,
139
+                                 MAX_PREFERRED_LENGTH, &nread, &nmax);
140
+    if (err && err != ERROR_MORE_DATA)
141
+    {
142
+        SetLastError (err);
143
+        MsgToEventLog (M_SYSERR, TEXT("NetUserGetLocalGroups"));
144
+        goto out;
145
+    }
146
+
147
+    admin_group[0] = SYSTEM_ADMIN_GROUP;
148
+#ifdef UNICODE
149
+    admin_group[1] = s->ovpn_admin_group;
150
+#else
151
+    tmp = NULL;
152
+    len = MultiByteToWideChar (CP_UTF8, 0, s->ovpn_admin_group, -1, NULL, 0);
153
+    if (len == 0 || (tmp = malloc (len*sizeof(WCHAR))) == NULL)
154
+    {
155
+        MsgToEventLog (M_SYSERR, TEXT("Failed to convert admin group name to WideChar"));
156
+        goto out;
157
+    }
158
+    MultiByteToWideChar (CP_UTF8, 0, s->ovpn_admin_group, -1, tmp, len);
159
+    admin_group[1] = tmp;
160
+#endif
161
+
162
+    /* Check if user's groups include any of the admin groups */
163
+    for (i = 0; i < nread; i++)
164
+    {
165
+        if ( wcscmp (groups[i].lgrui0_name, admin_group[0]) == 0 ||
166
+             wcscmp (groups[i].lgrui0_name, admin_group[1]) == 0
167
+           )
168
+        {
169
+            MsgToEventLog (M_INFO, TEXT("Authorizing user %s by virtue of membership in group %s"),
170
+                           username, groups[i].lgrui0_name);
171
+            ret = TRUE;
172
+            break;
173
+        }
174
+    }
175
+
176
+out:
177
+    if (groups)
178
+        NetApiBufferFree (groups);
179
+    free (tmp);
180
+
181
+    return ret;
182
+}
183
+
184
+/*
185
+ * Check whether option argv[0] is white-listed. If argv[0] == "--config",
186
+ * also check that argv[1], if present, passes CheckConfigPath().
187
+ * The caller should set argc to the number of valid elements in argv[] array.
188
+ */
189
+BOOL
190
+CheckOption (const WCHAR *workdir, int argc, WCHAR *argv[], const settings_t *s)
191
+{
192
+    /* Do not modify argv or *argv -- ideally it should be const WCHAR *const *, but alas...*/
193
+
194
+    if ( wcscmp (argv[0], L"--config") == 0            &&
195
+         argc > 1                                      &&
196
+         !CheckConfigPath (workdir, argv[1], s)
197
+       )
198
+    {
199
+        return FALSE;
200
+    }
201
+
202
+    /* option name starts at 2 characters from argv[i] */
203
+    if (OptionLookup (argv[0] + 2, white_list) == -1)  /* not found */
204
+        return FALSE;
205
+
206
+    return TRUE;
207
+}
0 208
new file mode 100644
... ...
@@ -0,0 +1,48 @@
0
+
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 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
20
+ *  along with this program (see the file COPYING included with this
21
+ *  distribution); if not, write to the Free Software Foundation, Inc.,
22
+ *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
23
+ */
24
+
25
+#ifndef VALIDATE_H
26
+#define VALIDATE_H
27
+
28
+#include "service.h"
29
+
30
+/* Authorized groups who can use any options and config locations */
31
+#define SYSTEM_ADMIN_GROUP TEXT("Administrators")
32
+#define OVPN_ADMIN_GROUP TEXT("OpenVPN Administrators")
33
+/* The last one may be reset in registry: HKLM\Software\OpenVPN\ovpn_admin_group */
34
+
35
+BOOL
36
+IsAuthorizedUser (SID *sid, settings_t *s);
37
+
38
+BOOL
39
+CheckOption (const WCHAR *workdir, int narg, WCHAR *argv[], const settings_t *s);
40
+
41
+static inline BOOL
42
+IsOption (const WCHAR *o)
43
+{
44
+    return (wcsncmp (o, L"--", 2) == 0);
45
+}
46
+
47
+#endif