1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
|
/* SPDX-FileCopyrightText: 2013-2023 Greenbone AG
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
/**
* @file
* @brief Check passwords against a list of pattern
*
* See \ref PWPOLICY_FILE_NAME for a syntax description of the pattern
* file.
*/
#include "pwpolicy.h"
#include <errno.h> /* for errno */
#include <glib.h> /* for g_strdup_printf, g_ascii_strcasecmp, g_free, ... */
#include <stdio.h> /* for fclose, fgets, fopen, FILE, ferror, EOF, getc */
#include <stdlib.h>
#include <string.h> /* for strstr, strlen, strncmp */
#ifndef DIM
#define DIM(v) (sizeof (v) / sizeof ((v)[0]))
#define DIMof(type, member) DIM (((type *) 0)->member)
#endif
#undef G_LOG_DOMAIN
/**
* @brief GLib log domain.
*/
#define G_LOG_DOMAIN "libgvm base"
/**
* @brief The name of the pattern file
*
* This file contains pattern with bad passphrases. The file is line
* based with maximum length of 255 bytes per line and expected to be
* in UTF-8 encoding. Each line may either be a comment line, a
* simple string, a regular expression or a processing instruction.
* The lines are parsed sequentially.
*
* *Comments* are indicated by a hash mark ('#') as the first non
* white-space character of a line followed immediately by a space or
* end of line. Such a comment line is completely ignored.
*
* *Simple strings* start after optional leading white-space. They
* are compared to the password under validation. The comparison is
* case insensitive for all ASCII characters.
*
* *Regular expressions* start after optional leading white-space with
* either a single slash ('/') or an exclamation mark ('!') directly
* followed by a slash. They extend to the end of the line but may be
* terminated with another slash which may then only be followed by
* more white-space. The regular expression are Perl Compatible
* Regular Expressions (PCRE) and are by default case insensitive. If
* the regular expression line starts with the exclamation mark, the
* match is reversed; i.e. an error is returned if the password does
* not match.
*
* *Processing instructions* are special comments to control the
* operation of the policy checking. The start like a comment but the
* hash mark is immediately followed by a plus ('+') signed, a
* keyword, an optional colon (':') and an optional value string. The
* following processing instructions are supported:
*
* #+desc[:] STRING
*
* This is used to return a meaningful error message. STRING is
* used a the description for all errors up to the next /desc/ or
* /nodesc/ processing instruction.
*
* #+nodesc
*
* This is syntactic sugar for /desc/ without a value. It
* switches back to a default error description (pattern file name
* and line number).
*
* #+search[:] FILENAME
*
* This searches the file with name FILENAME for a match. The
* comparison is case insensitive for all ASCII characters. This
* is a simple linear search and stops at the first match.
* Comments are not allowed in that file. A line in that file may
* not be longer than 255 characters. An example for such a file
* is "/usr/share/dict/words".
*
* #+username
*
* This is used to perform checks on the name/password
* combination. Currently this checks whether the password
* matches or is included in the password. It may eventually be
* extended to further tests.
*/
#define PWPOLICY_FILE_NAME GVM_SYSCONF_DIR "/pwpolicy.conf"
/**
* @brief Flag indicating that passwords are not checked.
*/
static gboolean disable_password_policy;
/**
* @return A malloced string to be returned on read and configuration
* errors.
*/
static char *
policy_checking_failed (void)
{
return g_strdup ("Password policy checking failed (internal error)");
}
/**
* @brief Check whether a string starts with a keyword
*
* Note that the keyword may optionally be terminated by a colon.
*
* @param string The string to check
* @param keyword The keyword
*
* @return NULL if the keyword is not found. If found a pointer into
* \p string to the value of the keyword with removed leading
* spaces is returned.
*/
static char *
is_keyword (char *string, const char *keyword)
{
int idx, slen;
char *tmp;
idx = strlen (keyword);
slen = strlen (string);
if (!strncmp (string, keyword, idx))
{
tmp = string + idx;
if (tmp - string > slen)
return NULL;
// skip optional:
if (*tmp == ':')
tmp++;
if (tmp - string > slen)
return NULL;
for (; tmp - string < slen && g_ascii_isspace (*tmp); tmp++)
{
// skip whitespace
}
return tmp;
}
return NULL;
}
/**
* @brief Search a file for a matching line
*
* This is a case insensitive search for a password in a file. The
* file is assumed to be a simple LF delimited list of words.
*
* @param fname Name of the file to search.
* @param password Password to search for.
*
* @return -1 if the file could not be opened or a read error
* occurred, 0 if password was not found and 1 if password was found.
*/
static int
search_file (const char *fname, const char *password)
{
FILE *fp;
int c;
char line[256];
fp = fopen (fname, "r");
if (!fp)
return -1;
while (fgets (line, DIM (line) - 1, fp))
{
size_t len;
len = strlen (line);
if (!len || line[len - 1] != '\n')
{
/* Incomplete last line or line too long. Eat until end of
line. */
while ((c = getc (fp)) != EOF && c != '\n')
;
continue;
}
line[--len] = 0; /* Chop the LF. */
if (len && line[len - 1] == '\r')
line[--len] = 0; /* Chop an optional CR. */
if (!len)
continue; /* Empty */
if (!g_ascii_strcasecmp (line, password))
{
fclose (fp);
return 1; /* Found. */
}
}
if (ferror (fp))
{
int save_errno = errno;
fclose (fp);
errno = save_errno;
return -1; /* Read error. */
}
fclose (fp);
return 0; /* Not found. */
}
/**
* @brief Parse one line of a pettern file
*
* @param line A null terminated buffer with the content of the line.
* The line terminator has already been stripped. It may
* be modified after return.
* @param fname The name of the pattern file for error reporting
* @param lineno The current line number for error reporting
* @param descp Pointer to a variable holding the current description
* string or NULL for no description.
* @param password The password to check.
* @param username The username to check.
*
* @return NULL on success or a malloced string with an error
* description.
*/
static char *
parse_pattern_line (char *line, const char *fname, int lineno, char **descp,
const char *password, const char *username)
{
char *ret = NULL;
char *p;
size_t n;
/* Skip leading spaces. */
while (g_ascii_isspace (*line))
line++;
if (!*line) /* Empty line. */
{
ret = NULL;
}
else if (*line == '#' && line[1] == '+') /* Processing instruction. */
{
line += 2;
p = is_keyword (line, "desc");
if (p)
{
g_free (*descp);
if (*p)
*descp = g_strdup (p);
else
*descp = NULL;
}
else if ((is_keyword (line, "nodesc")))
{
g_free (*descp);
*descp = NULL;
}
else if ((p = is_keyword (line, "search")))
{
int sret;
sret = search_file (p, password);
if (sret == -1)
{
g_warning ("error searching '%s' (requested at line %d): %s", p,
lineno, g_strerror (errno));
ret = policy_checking_failed ();
}
else if (sret && *descp)
ret = g_strdup_printf ("Weak password (%s)", *descp);
else if (sret)
ret = g_strdup_printf ("Weak password (found in '%s')", p);
else
ret = NULL;
}
else if (is_keyword (line, "username"))
{
/* Fixme: The include check is case sensitive and the strcmp
does only work with ascii. Changing this required a bit
more more (g_utf8_casefold) and also requires checking
for valid utf8 sequences in the password and all pattern. */
if (!username)
ret = NULL;
else if (!g_ascii_strcasecmp (password, username))
ret = g_strdup_printf ("Weak password (%s)",
"user name matches password");
else if (strstr (password, username))
ret = g_strdup_printf ("Weak password (%s)",
"user name is part of the password");
else if (strstr (username, password))
ret = g_strdup_printf ("Weak password (%s)",
"password is part of the user name");
else
ret = NULL;
}
else
{
g_warning ("error reading '%s', line %d: %s", fname, lineno,
"unknown processing instruction");
ret = policy_checking_failed ();
}
}
else if (*line == '#') /* Comment */
{
ret = NULL;
}
else if (*line == '/'
|| (*line == '!' && line[1] == '/')) /* Regular expression. */
{
int rev = (*line == '!');
if (rev)
line++;
line++;
n = strlen (line);
if (n && line[n - 1] == '/')
line[n - 1] = 0;
if (((!g_regex_match_simple (line, password, G_REGEX_CASELESS, 0)) ^ rev))
ret = NULL;
else if (*descp)
ret = g_strdup_printf ("Weak password (%s)", *descp);
else
ret =
g_strdup_printf ("Weak password (see '%s' line %d)", fname, lineno);
}
else /* Simple string. */
{
if (g_ascii_strcasecmp (line, password))
ret = NULL;
else if (*descp)
ret = g_strdup_printf ("Weak password (%s)", *descp);
else
ret =
g_strdup_printf ("Weak password (see '%s' line %d)", fname, lineno);
}
return ret;
}
/**
* @brief Validate a password against the pattern file
*
* @param[in] password The password to check
* @param[in] username The user name or NULL. This is used to check
* the passphrase against the user name.
*
* @return NULL on success or a malloced string with an error
* description.
*/
char *
gvm_validate_password (const char *password, const char *username)
{
const char *patternfile = PWPOLICY_FILE_NAME;
char *ret;
FILE *fp;
int lineno;
char line[256];
char *desc = NULL;
if (disable_password_policy)
return NULL;
if (!password || !*password)
return g_strdup ("Empty password");
fp = fopen (patternfile, "r");
if (!fp)
{
g_warning ("error opening '%s': %s", patternfile, g_strerror (errno));
return policy_checking_failed ();
}
lineno = 0;
ret = NULL;
while (fgets (line, DIM (line) - 1, fp))
{
size_t len;
lineno++;
len = strlen (line);
if (!len || line[len - 1] != '\n')
{
g_warning ("error reading '%s', line %d: %s", patternfile, lineno,
len ? "line too long" : "line without a LF");
ret = policy_checking_failed ();
break;
}
line[--len] = 0; /* Chop the LF. */
if (len && line[len - 1] == '\r')
line[--len] = 0; /* Chop an optional CR. */
ret = parse_pattern_line (line, patternfile, lineno, &desc, password,
username);
if (ret)
break;
bzero (line, sizeof (line));
}
fclose (fp);
g_free (desc);
return ret;
}
/**
* @brief Disable all password policy checking
*/
void
gvm_disable_password_policy (void)
{
disable_password_policy = TRUE;
g_warning ("Password policy checking has been disabled.");
}
|