Commit 3de67173de1b162ad967f67dc23e4a2663b94f9b
1 parent
63158cf5
Better fix to insecure password check (fixes #501)
Showing
5 changed files
with
64 additions
and
64 deletions
ChangeLog
| @@ -5,11 +5,11 @@ | @@ -5,11 +5,11 @@ | ||
| 5 | Fixes #499. | 5 | Fixes #499. |
| 6 | 6 | ||
| 7 | * By default, give an error if a user attempts to encrypt a file | 7 | * By default, give an error if a user attempts to encrypt a file |
| 8 | - with an empty owner password or an owner password that is the same | ||
| 9 | - as the user password. Such files are insecure. Most viewers either | ||
| 10 | - won't open such files or will not enforce security settings. To | ||
| 11 | - allow explicit creation of files like this, pass the new | ||
| 12 | - --allow-insecure option. Fixes #501. | 8 | + with a 256-bit key, a non-empty user password, and an empty owner |
| 9 | + password. Such files are insecure since they can be opened with no | ||
| 10 | + password. To allow explicit creation of files like this, pass the | ||
| 11 | + new --allow-insecure option. Thanks to github user RobK88 for a | ||
| 12 | + detailed analysis and for reporting this issue. Fixes #501. | ||
| 13 | 13 | ||
| 14 | 2021-02-02 Jay Berkenbilt <ejb@ql.org> | 14 | 2021-02-02 Jay Berkenbilt <ejb@ql.org> |
| 15 | 15 |
manual/qpdf-manual.xml
| @@ -1239,10 +1239,11 @@ make | @@ -1239,10 +1239,11 @@ make | ||
| 1239 | <para> | 1239 | <para> |
| 1240 | Either or both of the user password and the owner password may be | 1240 | Either or both of the user password and the owner password may be |
| 1241 | empty strings. Starting in qpdf 10.2, qpdf defaults to not | 1241 | empty strings. Starting in qpdf 10.2, qpdf defaults to not |
| 1242 | - allowing creation of PDF files with an empty owner password or an | ||
| 1243 | - owner password that matches the user password. If you want to | ||
| 1244 | - create such files, specify the encryption option | ||
| 1245 | - <option>--allow-insecure</option>, as described below. | 1242 | + allowing creation of PDF files with a non-empty user password, an |
| 1243 | + empty owner password, and a 256-bit key since such files can be | ||
| 1244 | + opened with no password. If you want to create such files, specify | ||
| 1245 | + the encryption option <option>--allow-insecure</option>, as | ||
| 1246 | + described below. | ||
| 1246 | </para> | 1247 | </para> |
| 1247 | <para> | 1248 | <para> |
| 1248 | The value for | 1249 | The value for |
| @@ -1252,25 +1253,6 @@ make | @@ -1252,25 +1253,6 @@ make | ||
| 1252 | fully permissive. | 1253 | fully permissive. |
| 1253 | </para> | 1254 | </para> |
| 1254 | <para> | 1255 | <para> |
| 1255 | - For all key lengths, the following options are available: | ||
| 1256 | - <variablelist> | ||
| 1257 | - <varlistentry> | ||
| 1258 | - <term><option>--allow-insecure</option></term> | ||
| 1259 | - <listitem> | ||
| 1260 | - <para> | ||
| 1261 | - From qpdf 10.2, qpdf defaults to not allowing creation of PDF | ||
| 1262 | - files where the owner password is blank or matches the user | ||
| 1263 | - password. Files created in this way are insecure and can't be | ||
| 1264 | - opened by some viewers. Users would ordinarily never want to | ||
| 1265 | - create such files. If you are using qpdf to intentionally | ||
| 1266 | - created strange files for testing (a definite valid use of | ||
| 1267 | - qpdf!), this option allows you to create such insecure files. | ||
| 1268 | - </para> | ||
| 1269 | - </listitem> | ||
| 1270 | - </varlistentry> | ||
| 1271 | - </variablelist> | ||
| 1272 | - </para> | ||
| 1273 | - <para> | ||
| 1274 | If <option><replaceable>key-length</replaceable></option> is 40, | 1256 | If <option><replaceable>key-length</replaceable></option> is 40, |
| 1275 | the following restriction options are available: | 1257 | the following restriction options are available: |
| 1276 | <variablelist> | 1258 | <variablelist> |
| @@ -1466,6 +1448,21 @@ make | @@ -1466,6 +1448,21 @@ make | ||
| 1466 | </listitem> | 1448 | </listitem> |
| 1467 | </varlistentry> | 1449 | </varlistentry> |
| 1468 | <varlistentry> | 1450 | <varlistentry> |
| 1451 | + <term><option>--allow-insecure</option></term> | ||
| 1452 | + <listitem> | ||
| 1453 | + <para> | ||
| 1454 | + From qpdf 10.2, qpdf defaults to not allowing creation of PDF | ||
| 1455 | + files where the user password is non-empty, the owner password | ||
| 1456 | + is empty, and a 256-bit key is in use. Files created in this | ||
| 1457 | + way are insecure since they can be opened without a password. | ||
| 1458 | + Users would ordinarily never want to create such files. If you | ||
| 1459 | + are using qpdf to intentionally created strange files for | ||
| 1460 | + testing (a definite valid use of qpdf!), this option allows | ||
| 1461 | + you to create such insecure files. | ||
| 1462 | + </para> | ||
| 1463 | + </listitem> | ||
| 1464 | + </varlistentry> | ||
| 1465 | + <varlistentry> | ||
| 1469 | <term><option>--force-V4</option></term> | 1466 | <term><option>--force-V4</option></term> |
| 1470 | <listitem> | 1467 | <listitem> |
| 1471 | <para> | 1468 | <para> |
| @@ -4877,15 +4874,16 @@ print "\n"; | @@ -4877,15 +4874,16 @@ print "\n"; | ||
| 4877 | <listitem> | 4874 | <listitem> |
| 4878 | <para> | 4875 | <para> |
| 4879 | By default, <command>qpdf</command> no longer allows | 4876 | By default, <command>qpdf</command> no longer allows |
| 4880 | - creation of encrypted PDF files whose owner password is | ||
| 4881 | - empty or matches the user password. The | ||
| 4882 | - <option>--allow-insecure</option>, specified inside the | ||
| 4883 | - <option>--encrypt</option> options, allows creation of such | ||
| 4884 | - files. Behavior changes in the CLI are avoided when | ||
| 4885 | - possible, but an exception was made here because this is | ||
| 4886 | - security-related. qpdf must always allow creation of weird | ||
| 4887 | - files for testing purposes, but it should not default to | ||
| 4888 | - letting users unknowingly create insecure files. | 4877 | + creation of encrypted PDF files whose user password is |
| 4878 | + non-empty and owner password is empty when a 256-bit key is | ||
| 4879 | + in use. The <option>--allow-insecure</option> option, | ||
| 4880 | + specified inside the <option>--encrypt</option> options, | ||
| 4881 | + allows creation of such files. Behavior changes in the CLI | ||
| 4882 | + are avoided when possible, but an exception was made here | ||
| 4883 | + because this is security-related. qpdf must always allow | ||
| 4884 | + creation of weird files for testing purposes, but it should | ||
| 4885 | + not default to letting users unknowingly create insecure | ||
| 4886 | + files. | ||
| 4889 | </para> | 4887 | </para> |
| 4890 | </listitem> | 4888 | </listitem> |
| 4891 | </itemizedlist> | 4889 | </itemizedlist> |
qpdf/qpdf.cc
| @@ -1082,7 +1082,6 @@ ArgParser::initOptionTable() | @@ -1082,7 +1082,6 @@ ArgParser::initOptionTable() | ||
| 1082 | 1082 | ||
| 1083 | t = &this->encrypt40_option_table; | 1083 | t = &this->encrypt40_option_table; |
| 1084 | (*t)["--"] = oe_bare(&ArgParser::argEndEncrypt); | 1084 | (*t)["--"] = oe_bare(&ArgParser::argEndEncrypt); |
| 1085 | - (*t)["allow-insecure"] = oe_bare(&ArgParser::argAllowInsecure); | ||
| 1086 | // The above 40-bit options are also 128-bit and 256-bit options, | 1085 | // The above 40-bit options are also 128-bit and 256-bit options, |
| 1087 | // so copy what we have so far to 128. Then continue separately | 1086 | // so copy what we have so far to 128. Then continue separately |
| 1088 | // with 128. We later copy 128 to 256. | 1087 | // with 128. We later copy 128 to 256. |
| @@ -1117,6 +1116,7 @@ ArgParser::initOptionTable() | @@ -1117,6 +1116,7 @@ ArgParser::initOptionTable() | ||
| 1117 | 1116 | ||
| 1118 | t = &this->encrypt256_option_table; | 1117 | t = &this->encrypt256_option_table; |
| 1119 | (*t)["force-R5"] = oe_bare(&ArgParser::arg256ForceR5); | 1118 | (*t)["force-R5"] = oe_bare(&ArgParser::arg256ForceR5); |
| 1119 | + (*t)["allow-insecure"] = oe_bare(&ArgParser::argAllowInsecure); | ||
| 1120 | 1120 | ||
| 1121 | t = &this->under_overlay_option_table; | 1121 | t = &this->under_overlay_option_table; |
| 1122 | (*t)[""] = oe_positional(&ArgParser::argUOpositional); | 1122 | (*t)[""] = oe_positional(&ArgParser::argUOpositional); |
| @@ -1333,10 +1333,6 @@ ArgParser::argHelp() | @@ -1333,10 +1333,6 @@ ArgParser::argHelp() | ||
| 1333 | << "\n" | 1333 | << "\n" |
| 1334 | << "Additional flags are dependent upon key length.\n" | 1334 | << "Additional flags are dependent upon key length.\n" |
| 1335 | << "\n" | 1335 | << "\n" |
| 1336 | - << " For all key lengths:\n" | ||
| 1337 | - << " --allow-insecure allow the owner password to be empty or the\n" | ||
| 1338 | - << " same as the user password\n" | ||
| 1339 | - << "\n" | ||
| 1340 | << " If 40:\n" | 1336 | << " If 40:\n" |
| 1341 | << "\n" | 1337 | << "\n" |
| 1342 | << " --print=[yn] allow printing\n" | 1338 | << " --print=[yn] allow printing\n" |
| @@ -1362,6 +1358,9 @@ ArgParser::argHelp() | @@ -1362,6 +1358,9 @@ ArgParser::argHelp() | ||
| 1362 | << " --force-V4 this option is not available with 256-bit keys\n" | 1358 | << " --force-V4 this option is not available with 256-bit keys\n" |
| 1363 | << " --use-aes this option is always on with 256-bit keys\n" | 1359 | << " --use-aes this option is always on with 256-bit keys\n" |
| 1364 | << " --force-R5 forces use of deprecated R=5 encryption\n" | 1360 | << " --force-R5 forces use of deprecated R=5 encryption\n" |
| 1361 | + << " --allow-insecure allow the owner password to be empty when the\n" | ||
| 1362 | + << " user password is not empty\n" | ||
| 1363 | + << "\n" | ||
| 1365 | << "\n" | 1364 | << "\n" |
| 1366 | << " print-opt may be:\n" | 1365 | << " print-opt may be:\n" |
| 1367 | << "\n" | 1366 | << "\n" |
| @@ -3394,15 +3393,21 @@ ArgParser::doFinalChecks() | @@ -3394,15 +3393,21 @@ ArgParser::doFinalChecks() | ||
| 3394 | } | 3393 | } |
| 3395 | 3394 | ||
| 3396 | if (o.encrypt && (! o.allow_insecure) && | 3395 | if (o.encrypt && (! o.allow_insecure) && |
| 3397 | - (o.owner_password.empty() || | ||
| 3398 | - (o.owner_password == o.user_password))) | ||
| 3399 | - { | ||
| 3400 | - usage("An encrypted PDF with an empty owner password or an" | ||
| 3401 | - " owner password that is the same as a user password" | ||
| 3402 | - " is insecure and can't be opened by some viewers. If you" | ||
| 3403 | - " really want to do this, you must also give the" | ||
| 3404 | - " --allow-insecure option before the -- that follows" | ||
| 3405 | - " --encrypt."); | 3396 | + (o.owner_password.empty() && |
| 3397 | + (! o.user_password.empty()) && | ||
| 3398 | + (o.keylen == 256))) | ||
| 3399 | + { | ||
| 3400 | + // Note that empty owner passwords for R < 5 are copied from | ||
| 3401 | + // the user password, so this lack of security is not an issue | ||
| 3402 | + // for those files. Also we are consider only the ability to | ||
| 3403 | + // open the file without a password to be insecure. We are not | ||
| 3404 | + // concerned about whether the viwer enforces security | ||
| 3405 | + // settings when the user and owner password match. | ||
| 3406 | + usage("A PDF with a non-empty user password and an empty owner" | ||
| 3407 | + " password encrypted with a 256-bit key is insecure as it" | ||
| 3408 | + " can be opened without a password. If you really want to" | ||
| 3409 | + " do this, you must also give the --allow-insecure option" | ||
| 3410 | + " before the -- that follows --encrypt."); | ||
| 3406 | } | 3411 | } |
| 3407 | 3412 | ||
| 3408 | if (o.require_outfile && o.outfilename && | 3413 | if (o.require_outfile && o.outfilename && |
qpdf/qtest/qpdf.test
| @@ -3464,7 +3464,7 @@ my @encrypted_files = | @@ -3464,7 +3464,7 @@ my @encrypted_files = | ||
| 3464 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], | 3464 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], |
| 3465 | ); | 3465 | ); |
| 3466 | 3466 | ||
| 3467 | -$n_tests += 9 + (2 * (@encrypted_files)) + (7 * (@encrypted_files - 6)) + 9; | 3467 | +$n_tests += 8 + (2 * (@encrypted_files)) + (7 * (@encrypted_files - 6)) + 9; |
| 3468 | 3468 | ||
| 3469 | $td->runtest("encrypted file", | 3469 | $td->runtest("encrypted file", |
| 3470 | {$td->COMMAND => "test_driver 2 encrypted-with-images.pdf"}, | 3470 | {$td->COMMAND => "test_driver 2 encrypted-with-images.pdf"}, |
| @@ -3482,17 +3482,12 @@ $td->runtest("recheck encrypted file", | @@ -3482,17 +3482,12 @@ $td->runtest("recheck encrypted file", | ||
| 3482 | $td->NORMALIZE_NEWLINES); | 3482 | $td->NORMALIZE_NEWLINES); |
| 3483 | 3483 | ||
| 3484 | $td->runtest("empty owner password", | 3484 | $td->runtest("empty owner password", |
| 3485 | - {$td->COMMAND => "qpdf --encrypt '' '' 128 -- minimal.pdf a.pdf"}, | ||
| 3486 | - {$td->REGEXP => ".*is insecure.*--allow-insecure.*", | ||
| 3487 | - $td->EXIT_STATUS => 2}, | ||
| 3488 | - $td->NORMALIZE_NEWLINES); | ||
| 3489 | -$td->runtest("matching user/owner password", | ||
| 3490 | - {$td->COMMAND => "qpdf --encrypt q q 128 -- minimal.pdf a.pdf"}, | 3485 | + {$td->COMMAND => "qpdf --encrypt u '' 256 -- minimal.pdf a.pdf"}, |
| 3491 | {$td->REGEXP => ".*is insecure.*--allow-insecure.*", | 3486 | {$td->REGEXP => ".*is insecure.*--allow-insecure.*", |
| 3492 | $td->EXIT_STATUS => 2}, | 3487 | $td->EXIT_STATUS => 2}, |
| 3493 | $td->NORMALIZE_NEWLINES); | 3488 | $td->NORMALIZE_NEWLINES); |
| 3494 | $td->runtest("allow insecure", | 3489 | $td->runtest("allow insecure", |
| 3495 | - {$td->COMMAND => "qpdf --encrypt '' '' 128 --allow-insecure --" . | 3490 | + {$td->COMMAND => "qpdf --encrypt u '' 256 --allow-insecure --" . |
| 3496 | " minimal.pdf a.pdf"}, | 3491 | " minimal.pdf a.pdf"}, |
| 3497 | {$td->STRING => "", $td->EXIT_STATUS => 0}, | 3492 | {$td->STRING => "", $td->EXIT_STATUS => 0}, |
| 3498 | $td->NORMALIZE_NEWLINES); | 3493 | $td->NORMALIZE_NEWLINES); |
| @@ -3630,7 +3625,7 @@ foreach my $d (@encrypted_files) | @@ -3630,7 +3625,7 @@ foreach my $d (@encrypted_files) | ||
| 3630 | $enc_json =~ s/---upm---/$upm/; | 3625 | $enc_json =~ s/---upm---/$upm/; |
| 3631 | 3626 | ||
| 3632 | my $eflags = "-encrypt \"$upass\" \"$opass\" $bits $xeflags --"; | 3627 | my $eflags = "-encrypt \"$upass\" \"$opass\" $bits $xeflags --"; |
| 3633 | - if (($opass eq "") || ($opass eq $upass)) | 3628 | + if (($opass eq "") && ($bits == 256)) |
| 3634 | { | 3629 | { |
| 3635 | $eflags =~ s/--$/--allow-insecure --/; | 3630 | $eflags =~ s/--$/--allow-insecure --/; |
| 3636 | } | 3631 | } |
| @@ -3892,7 +3887,7 @@ foreach my $d (['--force-V4', 'V4'], | @@ -3892,7 +3887,7 @@ foreach my $d (['--force-V4', 'V4'], | ||
| 3892 | my ($args, $out) = @$d; | 3887 | my ($args, $out) = @$d; |
| 3893 | $td->runtest("encrypt $args", | 3888 | $td->runtest("encrypt $args", |
| 3894 | {$td->COMMAND => "qpdf --static-aes-iv --static-id" . | 3889 | {$td->COMMAND => "qpdf --static-aes-iv --static-id" . |
| 3895 | - " --encrypt '' '' 128 $args --allow-insecure --" . | 3890 | + " --encrypt '' '' 128 $args --" . |
| 3896 | " enc-base.pdf a.pdf"}, | 3891 | " enc-base.pdf a.pdf"}, |
| 3897 | {$td->STRING => "", $td->EXIT_STATUS => 0}); | 3892 | {$td->STRING => "", $td->EXIT_STATUS => 0}); |
| 3898 | $td->runtest("check output", | 3893 | $td->runtest("check output", |
qpdf/qtest/qpdf/insecure-passwords.out
| 1 | checking a.pdf | 1 | checking a.pdf |
| 2 | -PDF Version: 1.4 | ||
| 3 | -R = 3 | 2 | +PDF Version: 1.7 extension level 8 |
| 3 | +R = 6 | ||
| 4 | P = -4 | 4 | P = -4 |
| 5 | User password = | 5 | User password = |
| 6 | Supplied password is owner password | 6 | Supplied password is owner password |
| 7 | -Supplied password is user password | ||
| 8 | extract for accessibility: allowed | 7 | extract for accessibility: allowed |
| 9 | extract for any purpose: allowed | 8 | extract for any purpose: allowed |
| 10 | print low resolution: allowed | 9 | print low resolution: allowed |
| @@ -14,6 +13,9 @@ modify forms: allowed | @@ -14,6 +13,9 @@ modify forms: allowed | ||
| 14 | modify annotations: allowed | 13 | modify annotations: allowed |
| 15 | modify other: allowed | 14 | modify other: allowed |
| 16 | modify anything: allowed | 15 | modify anything: allowed |
| 16 | +stream encryption method: AESv3 | ||
| 17 | +string encryption method: AESv3 | ||
| 18 | +file encryption method: AESv3 | ||
| 17 | File is not linearized | 19 | File is not linearized |
| 18 | No syntax or stream encoding errors found; the file may still contain | 20 | No syntax or stream encoding errors found; the file may still contain |
| 19 | errors that qpdf cannot detect | 21 | errors that qpdf cannot detect |