diff --git a/fn/auth.php b/fn/auth.php index 14fc55f..4f321e2 100644 --- a/fn/auth.php +++ b/fn/auth.php @@ -106,13 +106,31 @@ function authDeleteUser($user_id) { foreach (query('select', 'sites', ['username' => $user_id]) as $site) htDeleteSite($site['address'], $site['type'], $user_id); - exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['rm_path'] . ' -r ' . CONF['ht']['tor_keys_path'] . '/' . $user_id, result_code: $code); + exescape([ + CONF['ht']['sudo_path'], + '-u', + CONF['ht']['tor_user'], + '--', + CONF['ht']['rm_path'], + '-r', + '--', + CONF['ht']['tor_keys_path'] . '/' . $user_id, + ], result_code: $code); if ($code !== 0) output(500, 'Can\'t remove Tor keys directory.'); removeDirectory(CONF['ht']['tor_config_path'] . '/' . $user_id); - exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['sftpgo_user'] . ' ' . CONF['ht']['rm_path'] . ' -r ' . CONF['ht']['ht_path'] . '/fs/' . $user_id, result_code: $code); + exescape([ + CONF['ht']['sudo_path'], + '-u', + CONF['ht']['sftpgo_user'], + '--', + CONF['ht']['rm_path'], + '-r', + '--', + CONF['ht']['ht_path'] . '/fs/' . $user_id + ], result_code: $code); if ($code !== 0) output(500, 'Can\'t remove user\'s directory.'); } diff --git a/fn/common.php b/fn/common.php index 3d255d9..0002ca1 100644 --- a/fn/common.php +++ b/fn/common.php @@ -16,6 +16,11 @@ function output($code, $msg = '', $logs = [''], $data = []) { exit(); } +function exescape(array $args, array &$output = NULL, int &$result_code = NULL): int { + exec('2>&1 ' . implode(' ', array_map('escapeshellarg', $args)), $output, $result_code); + return $result_code; +} + function insert($table, $values) { $query = 'INSERT INTO "' . $table . '"('; diff --git a/fn/dns.php b/fn/dns.php index b95f73c..ef4f803 100644 --- a/fn/dns.php +++ b/fn/dns.php @@ -15,42 +15,53 @@ function parseZoneFile($zone_content, $types, $filter_domain = false) { return $parsed_zone_content; } -function knotcConfExec($cmds) { - exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 conf-begin', $output['begin'], $code['begin']); +function knotc(array $cmds, array &$output = NULL, int &$return_code = NULL): void { + exescape([ + CONF['dns']['knotc_path'], + '--blocking', + '--timeout', + '3', + '--', + ...$cmds, + ], $output, $return_code); +} + +function knotcConfExec(array $cmds): void { + knotc(['conf-begin'], $output['begin'], $code['begin']); if ($code['begin'] !== 0) output(500, 'knotcConfExec: knotc failed with exit code ' . $code['begin'] . ': ' . $output['begin'][0] . '.'); foreach ($cmds as $cmd) { - exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 conf-' . $cmd, $output['op'], $code['op']); + knotc($cmd, $output['op'], $code['op']); if ($code['op'] !== 0) { - exec(CONF['dns']['knotc_path'] . ' --blocking conf-abort'); + knotc(['conf-abort']); output(500, 'knotcConfExec: knotc failed with exit code ' . $code['op'] . ': ' . $output['op'][0] . '.'); } } - exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 conf-commit', $output['commit'], $code['commit']); + knotc(['conf-commit'], $output['commit'], $code['commit']); if ($code['commit'] !== 0) { - exec(CONF['dns']['knotc_path'] . ' --blocking conf-abort'); + knotc(['conf-abort']); output(500, 'knotcConfExec: knotc failed with exit code ' . $code['commit'] . ': ' . $output['commit'][0] . '.'); } } -function knotcZoneExec($zone, $cmd, $action = NULL) { +function knotcZoneExec(string $zone, array $cmd, string $action = NULL) { $action = checkAction($action ?? $_POST['action']); - exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 zone-begin ' . $zone, $output['begin'], $code['begin']); + knotc(['zone-begin', $zone], $output['begin'], $code['begin']); if ($code['begin'] !== 0) output(500, 'knotcZoneExec: knotc failed with exit code ' . $code['begin'] . ': ' . $output['begin'][0] . '.'); - exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 zone-' . $action . 'set ' . $zone . ' ' . implode(' ', $cmd), $output['op'], $code['op']); + knotc(['zone-' . $action . 'set', $zone, ...$cmd], $output['op'], $code['op']); if ($code['op'] !== 0) { - exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 zone-abort ' . $zone); + knotc(['zone-abort', $zone]); output(500, 'knotcZoneExec: knotc failed with exit code ' . $code['op'] . ': ' . $output['op'][0] . '.'); } - exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 zone-commit ' . $zone, $output['commit'], $code['commit']); + knotc(['zone-commit', $zone], $output['commit'], $code['commit']); if ($code['commit'] !== 0) { - exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 zone-abort ' . $zone); + knotc(['zone-abort', $zone]); output(500, 'knotcZoneExec: knotc failed with exit code ' . $code['commit'] . ': ' . $output['commit'][0] . '.'); } } diff --git a/fn/ht.php b/fn/ht.php index caee80d..8b9e3d5 100644 --- a/fn/ht.php +++ b/fn/ht.php @@ -9,7 +9,15 @@ function htSetupUserFs($id) { output(500, 'Can\'t create user directory.'); if (chmod(CONF['ht']['ht_path'] . '/fs/' . $id, 0775) !== true) output(500, 'Can\'t chmod user directory.'); - exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['chgrp_path'] . ' ' . CONF['ht']['sftpgo_group'] . ' ' . CONF['ht']['ht_path'] . '/fs/' . $id . ' --no-dereference', result_code: $code); + exescape([ + CONF['ht']['sudo_path'], + '--', + CONF['ht']['chgrp_path'], + '--no-dereference', + '--', + CONF['ht']['sftpgo_group'], + CONF['ht']['ht_path'] . '/fs/' . $id, + ], result_code: $code); if ($code !== 0) output(500, 'Can\'t change user directory group.'); @@ -20,7 +28,16 @@ function htSetupUserFs($id) { output(500, 'Can\'t chmod Tor config directory.'); // Setup Tor keys directory - exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['mkdir_path'] . ' --mode=0700 ' . CONF['ht']['tor_keys_path'] . '/' . $id, result_code: $code); + exescape([ + CONF['ht']['sudo_path'], + '-u', + CONF['ht']['tor_user'], + '--', + CONF['ht']['mkdir_path'], + '--mode=0700', + '--', + CONF['ht']['tor_keys_path'] . '/' . $id, + ], result_code: $code); if ($code !== 0) output(500, 'Can\'t create Tor keys directory.'); } @@ -93,19 +110,39 @@ function htDeleteSite($address, $type, $user_id) { output(500, 'Failed to delete Tor configuration.'); // Reload Tor - exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['tor_reload_cmd'], result_code: $code); + exescape([ + CONF['ht']['sudo_path'], + '--', + ...explode(' ', CONF['ht']['tor_reload_cmd']), + ], result_code: $code); if ($code !== 0) output(500, 'Failed to reload Tor.'); // Delete Tor keys - exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['rm_path'] . ' -r ' . CONF['ht']['tor_keys_path'] . '/' . $user_id . '/' . $dir, result_code: $code); + exescape([ + CONF['ht']['sudo_path'], + '-u', + CONF['ht']['tor_user'], + '--', + CONF['ht']['rm_path'], + '-r', + '--', + CONF['ht']['tor_keys_path'] . '/' . $user_id . '/' . $dir, + ], result_code: $code); if ($code !== 0) output(500, 'Failed to delete Tor keys.'); } if ($type === 'dns') { // Delete Let's Encrypt certificate - exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['certbot_path'] . ' delete --quiet --cert-name ' . $address, result_code: $code); + exescape([ + CONF['ht']['sudo_path'], + CONF['ht']['certbot_path'], + 'delete', + '--quiet', + '--cert-name', + $address, + ], result_code: $code); if ($code !== 0) output(500, 'Certbot failed to delete the Let\'s Encrypt certificate.'); } diff --git a/fn/ns.php b/fn/ns.php index e6c70c4..a93e82c 100644 --- a/fn/ns.php +++ b/fn/ns.php @@ -50,14 +50,24 @@ function nsCheckZonePossession($zone) { function nsDeleteZone($zone, $user_id) { // Remove from Knot configuration - knotcConfExec(["unset 'zone[$zone]'"]); + knotcConfExec([['conf-unset', 'zone[' . $zone . ']']]); // Remove Knot zone file if (unlink(CONF['ns']['knot_zones_path'] . '/' . $zone . 'zone') !== true) output(500, 'Failed to remove Knot zone file.'); // Remove Knot related data - exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 --force zone-purge ' . $zone . ' +orphan', result_code: $code); + exescape([ + CONF['dns']['knotc_path'], + '--blocking', + '--timeout', + '3', + '--force', + '--', + 'zone-purge', + $zone, + '+orphan', + ], result_code: $code); if ($code !== 0) output(500, 'Failed to purge zone data.'); diff --git a/jobs/check.php b/jobs/check.php index fa09715..733447b 100644 --- a/jobs/check.php +++ b/jobs/check.php @@ -12,7 +12,7 @@ const SUFFIX = 'test.servnest.test.'; const TOR_PROXY = 'socks5h://127.0.0.1:9050'; -exec(CONF['dns']['kdig_path'] . ' torproject.org AAAA', $output, $return_code); +exescape([CONF['dns']['kdig_path'], 'torproject.org', 'AAAA'], $output, $return_code); if (preg_match('/^;; Flags: qr rd ra ad;/Dm', implode("\n", $output)) !== 1) exit('Unable to do a DNSSEC-validated DNS query.' . LF); @@ -101,7 +101,12 @@ function testReg() { 'domain' => $domain, 'ns' => 'ns1.servnest.invalid.', ]); - exec(CONF['dns']['kdig_path'] . ' @' . CONF['reg']['address'] . ' ' . $domain . ' NS', $output); + exescape([ + CONF['dns']['kdig_path'], + '@' . CONF['reg']['address'], + $domain, + 'NS', + ], $output); if (preg_match('/[ \t]+ns1\.servnest\.invalid\.$/Dm', implode(LF, $output)) !== 1) exit('Error: /reg/ns: NS record not set' . LF); @@ -149,7 +154,12 @@ function testNs($domain) { 'tag' => 'issue', 'value' => 'letsencrypt.org', ]); - exec(CONF['dns']['kdig_path'] . ' @' . CONF['reg']['address'] . ' ' . $domain . ' CAA', $output); + exescape([ + CONF['dns']['kdig_path'], + '@' . CONF['reg']['address'], + $domain, + 'CAA', + ], $output); if (preg_match('/^' . preg_quote($domain, '/') . '[ \t]+7200[ \t]+IN[ \t]+CAA[ \t]+0[ \t]+issue[ \t]+"letsencrypt\.org"$/Dm', implode(LF, $output)) !== 1) exit('Error: /ns/caa: CAA record not set' . LF); @@ -158,7 +168,12 @@ function testNs($domain) { 'zone-content' => 'aaaa.' . $domain . ' 3600 AAAA ' . CONF['ht']['ipv6_address'] . "\r\n" . '@ 86400 NS ' . CONF['ns']['servers'][0] . "\r\n", ]); - exec(CONF['dns']['kdig_path'] . ' @' . CONF['reg']['address'] . ' aaaa.' . $domain . ' AAAA', $output); + exescape([ + CONF['dns']['kdig_path'], + '@' . CONF['reg']['address'], + 'aaaa.' . $domain, + 'AAAA', + ], $output); if (preg_match('/[ \t]+' . preg_quote(CONF['ht']['ipv6_address'], '/') . '$/Dm', implode(LF, $output)) !== 1) exit('Error: /ns/edit: AAAA record not set' . LF); } diff --git a/pg-act/ht/add-dns.php b/pg-act/ht/add-dns.php index 89609c3..561aee0 100644 --- a/pg-act/ht/add-dns.php +++ b/pg-act/ht/add-dns.php @@ -30,7 +30,14 @@ checkAuthToken($matches[1], $matches[2]); rateLimit(); -exec('2>&1 ' . CONF['ht']['sudo_path'] . ' ' . CONF['ht']['certbot_path'] . ' certonly' . (($_SESSION['type'] === 'approved') ? '' : ' --test-cert') . ' --domain ' . $_POST['domain'], $output, $returnCode); +exescape([ + CONF['ht']['sudo_path'], + CONF['ht']['certbot_path'], + 'certonly', + '--domain', + $_POST['domain'], + ...(($_SESSION['type'] === 'approved') ? [] : ['--test-cert']), +], $output, $returnCode); if ($returnCode !== 0) output(500, 'Certbot failed to get a Let\'s Encrypt certificate.', $output); diff --git a/pg-act/ht/add-onion.php b/pg-act/ht/add-onion.php index 86ff98e..f90cc35 100644 --- a/pg-act/ht/add-onion.php +++ b/pg-act/ht/add-onion.php @@ -15,16 +15,29 @@ if (chmod($torConfFile, 0644) !== true) output(500, 'Failed to give correct permissions to new Tor configuration file.'); // Reload Tor -exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['tor_reload_cmd'], result_code: $code); +exescape([ + CONF['ht']['sudo_path'], + '--', + ...explode(' ', CONF['ht']['tor_reload_cmd']), +], result_code: $code); if ($code !== 0) output(500, 'Failed to reload Tor.'); usleep(10000); // Get the hostname generated by Tor -$onion = exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['cat_path'] . ' ' . CONF['ht']['tor_keys_path'] . '/' . $_SESSION['id'] . '/' . $_POST['dir'] . '/hostname', result_code: $code); +exescape([ + CONF['ht']['sudo_path'], + '-u', + CONF['ht']['tor_user'], + '--', + CONF['ht']['cat_path'], + '--', + CONF['ht']['tor_keys_path'] . '/' . $_SESSION['id'] . '/' . $_POST['dir'] . '/hostname', +], $output, $code); if ($code !== 0) output(500, 'Unable to read hostname file.'); +$onion = $output[0]; if (preg_match('/^[0-9a-z]{56}\.onion$/D', $onion) !== 1) output(500, 'No onion address found.'); diff --git a/pg-act/ns/edit.php b/pg-act/ns/edit.php index 74db8a0..5ee0181 100644 --- a/pg-act/ns/edit.php +++ b/pg-act/ns/edit.php @@ -39,22 +39,22 @@ if (isset($_POST['zone-content'])) { // Update zone ratelimit(); - exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 zone-freeze ' . $_POST['zone'], $output, $return_code); + knotc(['zone-freeze', $_POST['zone']], $output, $return_code); if ($return_code !== 0) output(500, 'Failed to freeze zone file.', $output); - exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 zone-flush ' . $_POST['zone'], $output, $return_code); + knotc(['zone-flush', $_POST['zone']], $output, $return_code); if ($return_code !== 0) output(500, 'Failed to flush zone file.', $output); if (file_put_contents(CONF['ns']['knot_zones_path'] . '/' . $_POST['zone'] . 'zone', $new_zone_content) === false) output(500, 'Failed to write zone file.'); - exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 zone-reload ' . $_POST['zone'], $output, $return_code); + knotc(['zone-reload', $_POST['zone']], $output, $return_code); if ($return_code !== 0) output(500, 'Failed to reload zone file.', $output); - exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 zone-thaw ' . $_POST['zone'], $output, $return_code); + knotc(['zone-thaw', $_POST['zone']], $output, $return_code); if ($return_code !== 0) output(500, 'Failed to thaw zone file.', $output); diff --git a/pg-act/ns/zone-add.php b/pg-act/ns/zone-add.php index e711d34..092e870 100644 --- a/pg-act/ns/zone-add.php +++ b/pg-act/ns/zone-add.php @@ -5,7 +5,13 @@ $_POST['domain'] = formatAbsoluteDomain($_POST['domain']); if (query('select', 'zones', ['zone' => $_POST['domain']], 'zone') !== []) output(403, _('This zone already exists on the service.')); -exec(CONF['dns']['kdig_path'] . ' ' . ltrim(strstr($_POST['domain'], '.'), '.') . ' NS +short' . (CONF['ns']['local_only_check'] ? (' @' . CONF['reg']['address']) : ''), $parentAuthoritatives, $code); +exescape([ + CONF['dns']['kdig_path'], + ltrim(strstr($_POST['domain'], '.'), '.'), + 'NS', + '+short', + ...(CONF['ns']['local_only_check'] ? ['@' . CONF['reg']['address']] : []), +], $parentAuthoritatives, $code); if ($code !== 0) output(500, 'Unable to query parent name servers.'); if ($parentAuthoritatives === []) @@ -13,7 +19,13 @@ if ($parentAuthoritatives === []) foreach ($parentAuthoritatives as $parentAuthoritative) checkAbsoluteDomainFormat($parentAuthoritative); -exec(CONF['dns']['kdig_path'] . ' ' . $_POST['domain'] . ' NS @' . (CONF['ns']['local_only_check'] ? CONF['reg']['address'] : $parentAuthoritatives[0]) . ' +noidn', $results); +exescape([ + CONF['dns']['kdig_path'], + $_POST['domain'], + 'NS', + '@' . (CONF['ns']['local_only_check'] ? CONF['reg']['address'] : $parentAuthoritatives[0]), + '+noidn' +], $results); if (preg_match('/^' . preg_quote($_POST['domain'], '/') . '[\t ]+[0-9]{1,8}[\t ]+IN[\t ]+NS[\t ]+(?[0-9a-f]{8})-(?[0-9a-f]{32})\._domain-verification\.' . preg_quote(SERVER_NAME, '/') . '\.$/Dm', implode(LF, $results), $matches) !== 1) output(403, _('NS authentication record not found.')); @@ -47,8 +59,8 @@ if (chmod($knotZonePath, 0660) !== true) output(500, 'Failed to chmod new zone file.'); knotcConfExec([ - "set 'zone[" . $_POST['domain'] . "]'", - "set 'zone[" . $_POST['domain'] . "].template' 'servnest'", + ['conf-set', 'zone[' . $_POST['domain'] . ']'], + ['conf-set', 'zone[' . $_POST['domain'] . '].template', 'servnest'], ]); output(200, _('Zone created.')); diff --git a/pg-act/reg/ds.php b/pg-act/reg/ds.php index a656165..bbfd326 100644 --- a/pg-act/reg/ds.php +++ b/pg-act/reg/ds.php @@ -1,20 +1,18 @@ algo.'); +if (!in_array($_POST['algo'], ['8', '13', '14', '15', '16'], true)) + output(403, 'Wrong value for algo.'); $_POST['keytag'] = intval($_POST['keytag']); -if ((!preg_match('/^[0-9]{1,6}$/D', $_POST['keytag'])) OR !($_POST['keytag'] >= 1) OR !($_POST['keytag'] <= 65535)) +if ((preg_match('/^[0-9]{1,6}$/D', $_POST['keytag'])) !== 1 OR !($_POST['keytag'] >= 1) OR !($_POST['keytag'] <= 65535)) output(403, 'Wrong value for keytag.'); if ($_POST['dt'] !== '2' AND $_POST['dt'] !== '4') output(403, 'Wrong value for dt.'); +if (preg_match('/^(?:[0-9a-fA-F]{64}|[0-9a-fA-F]{96})$/D', $_POST['key']) !== 1) + output(403, 'Wrong value for key.'); + regCheckDomainPossession($_POST['zone']); rateLimit(); diff --git a/pg-act/reg/transfer.php b/pg-act/reg/transfer.php index 6a44150..f1ffdd9 100644 --- a/pg-act/reg/transfer.php +++ b/pg-act/reg/transfer.php @@ -11,7 +11,13 @@ $domain = formatAbsoluteDomain($_POST['subdomain'] . '.' . $_POST['suffix']); if (query('select', 'registry', ['username' => $_SESSION['id'], 'domain' => $domain], 'domain') !== []) output(403, _('The current account already owns this domain.')); -exec(CONF['dns']['kdig_path'] . ' ' . $domain . ' NS @' . CONF['reg']['address'] . ' +noidn', $results, $code); +exescape([ + CONF['dns']['kdig_path'], + $domain, + 'NS', + '@' . CONF['reg']['address'], + '+noidn', +], $results, $code); if ($code !== 0) output(500, 'Unable to query registry\'s name servers.'); if (preg_match('/^' . preg_quote($domain, '/') . '[\t ]+[0-9]{1,8}[\t ]+IN[\t ]+NS[\t ]+(?[0-9a-f]{8})-(?[0-9a-f]{32})\._transfer-verification\.' . preg_quote(SERVER_NAME, '/') . '\.$/Dm', implode(LF, $results), $matches) !== 1) diff --git a/pg-view/reg/ds.php b/pg-view/reg/ds.php index ef5dce0..ef1a6ae 100644 --- a/pg-view/reg/ds.php +++ b/pg-view/reg/ds.php @@ -51,7 +51,7 @@ foreach (regListUserDomains() as $domain)

- +