Improve security and PubKey handling for SFTP users.

This commit is contained in:
Buster "Silver Eagle" Neece 2022-05-09 09:05:34 -05:00
parent f5c73fb108
commit 6f1402c65f
No known key found for this signature in database
GPG Key ID: 9FC8B9E008872109
4 changed files with 90 additions and 35 deletions

View File

@ -66,6 +66,7 @@
"php-di/php-di": "^6.0",
"php-di/slim-bridge": "^3.0",
"phpmyadmin/motranslator": "^5.3",
"phpseclib/phpseclib": "^3.0",
"psr/http-factory": ">1",
"psr/simple-cache": ">1",
"ramsey/uuid": "^4.0",

2
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "4b8224fea5f9405d7270b1719c1dc791",
"content-hash": "ee39dba284c5c3f58875350e539c3826",
"packages": [
{
"name": "aws/aws-crt-php",

View File

@ -8,14 +8,13 @@ use App\Console\Command\CommandAbstract;
use App\Entity\SftpUser;
use Brick\Math\BigInteger;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use const JSON_NUMERIC_CHECK;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
#[AsCommand(
name: 'azuracast:internal:sftp-auth',
@ -25,6 +24,7 @@ class SftpAuthCommand extends CommandAbstract
{
public function __construct(
protected EntityManagerInterface $em,
protected LoggerInterface $logger,
) {
parent::__construct();
}
@ -32,39 +32,80 @@ class SftpAuthCommand extends CommandAbstract
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$errorResponse = json_encode(['username' => ''], JSON_THROW_ON_ERROR);
$username = getenv('SFTPGO_AUTHD_USERNAME') ?: null;
$password = getenv('SFTPGO_AUTHD_PASSWORD') ?: null;
$pubKey = getenv('SFTPGO_AUTHD_PUBLIC_KEY') ?: null;
$username = getenv('SFTPGO_AUTHD_USERNAME') ?: '';
$password = getenv('SFTPGO_AUTHD_PASSWORD') ?: '';
$pubKey = getenv('SFTPGO_AUTHD_PUBLIC_KEY') ?: '';
$sftpUser = $this->em->getRepository(SftpUser::class)->findOneBy(['username' => $username]);
if ($sftpUser instanceof SftpUser && $sftpUser->authenticate($password, $pubKey)) {
$storageLocation = $sftpUser->getStation()->getMediaStorageLocation();
$quotaRaw = $storageLocation->getStorageQuotaBytes();
$quota = ($quotaRaw instanceof BigInteger)
? (string)$quotaRaw
: 0;
$row = [
'status' => 1,
'username' => $sftpUser->getUsername(),
'expiration_date' => 0,
'home_dir' => $storageLocation->getPath(),
'uid' => 0,
'gid' => 0,
'quota_size' => $quota,
'permissions' => [
'/' => ['*'],
],
];
$io->write(json_encode($row, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK));
return 0;
if (empty($username)) {
$io->write($errorResponse);
return 1;
}
$io->write(json_encode(['username' => ''], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES));
return 1;
$sftpUser = $this->em->getRepository(SftpUser::class)->findOneBy(['username' => $username]);
if (!($sftpUser instanceof SftpUser)) {
$this->logger->notice(
sprintf(
'SFTP user "%s" not found.',
$username
)
);
$io->write($errorResponse);
return 1;
}
if (!$sftpUser->authenticate($password, $pubKey)) {
$this->logger->notice(
sprintf(
'SFTP user "%s" could not authenticate.',
$username
),
[
'hasPassword' => !empty($password),
'hasPubKey' => !empty($pubKey),
]
);
$io->write($errorResponse);
return 1;
}
$storageLocation = $sftpUser->getStation()->getMediaStorageLocation();
if (!$storageLocation->isLocal()) {
$this->logger->error(
sprintf(
'SFTP login failed for user "%s": Storage Location %s is not local.',
$username,
$storageLocation
)
);
$io->write($errorResponse);
return 1;
}
$quotaRaw = $storageLocation->getStorageQuotaBytes();
$quota = ($quotaRaw instanceof BigInteger)
? (string)$quotaRaw
: 0;
$row = [
'status' => 1,
'username' => $sftpUser->getUsername(),
'expiration_date' => 0,
'home_dir' => $storageLocation->getPath(),
'uid' => 0,
'gid' => 0,
'quota_size' => $quota,
'permissions' => [
'/' => ['*'],
],
];
$io->write(json_encode($row, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK));
return 0;
}
}

View File

@ -9,6 +9,7 @@ use App\Entity\Interfaces\IdentifiableEntityInterface;
use App\Validator\Constraints\UniqueEntity;
use Doctrine\ORM\Mapping as ORM;
use OpenApi\Attributes as OA;
use phpseclib3\Crypt\PublicKeyLoader;
use Symfony\Component\Validator\Constraints as Assert;
use const PASSWORD_ARGON2ID;
@ -101,7 +102,9 @@ class SftpUser implements IdentifiableEntityInterface
$pubKeysRaw = trim($this->publicKeys);
if (!empty($pubKeysRaw)) {
return array_filter(array_map('trim', explode("\n", $pubKeysRaw)));
return array_filter(
array_map([$this, 'cleanPublicKey'], explode("\n", $pubKeysRaw))
);
}
return [];
@ -120,9 +123,19 @@ class SftpUser implements IdentifiableEntityInterface
if (!empty($pubKey)) {
$pubKeys = $this->getPublicKeysArray();
return in_array($pubKey, $pubKeys, true);
return in_array($this->cleanPublicKey($pubKey), $pubKeys, true);
}
return false;
}
public function cleanPublicKey(string $pubKeyRaw): ?string
{
try {
$pkObj = PublicKeyLoader::loadPublicKey(trim($pubKeyRaw));
return trim($pkObj->toString('OpenSSH', ['comment' => '']));
} catch (\Exception) {
return null;
}
}
}