Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ before starting to add changes. Use example [placed in the end of the page](#exa

## [Unreleased]

- Fix IP validation in digital signature file download (CIDR support)

## [5.0.0] 2025-11-18

- [PR-192](https://github.com/OS2Forms/os2forms/pull/192)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManager;
use Drupal\os2forms_digital_signature\Form\SettingsForm;
use Symfony\Component\HttpFoundation\IpUtils;

/**
* Implements hook_cron().
Expand Down Expand Up @@ -57,21 +58,54 @@ function os2forms_digital_signature_file_download($uri) {
$config = \Drupal::config(SettingsForm::$configName);
$allowedIps = $config->get('os2forms_digital_signature_submission_allowed_ips');

$allowedIpsArr = explode(',', $allowedIps);
$remoteIp = Drupal::request()->getClientIp();
$allowedIpsArr = array_map('trim', explode(',', $allowedIps));
// Remove empty entries (e.g. from trailing comma or empty config).
$allowedIpsArr = array_filter($allowedIpsArr);
$remoteIp = \Drupal::request()->getClientIp();

// IP list is empty, or request IP is allowed.
if (empty($allowedIpsArr) || in_array($remoteIp, $allowedIpsArr)) {
// IP list is empty, allow access.
if (empty($allowedIpsArr)) {
$basename = basename($uri);
return [
'Content-disposition' => 'attachment; filename="' . $basename . '"',
];
}

// Otherwise - Deny access.
// Check if remote IP matches any allowed IP or CIDR range.
foreach ($allowedIpsArr as $allowedIp) {
if ($remoteIp === $allowedIp || os2forms_digital_signature_ip_in_cidr($remoteIp, $allowedIp)) {
$basename = basename($uri);
return [
'Content-disposition' => 'attachment; filename="' . $basename . '"',
];
}
}

// Deny access and log warning.
\Drupal::logger('os2forms_digital_signature')->warning('File download denied for IP @ip on URI @uri. Allowed IPs: @allowed', [
'@ip' => $remoteIp,
'@uri' => $uri,
'@allowed' => $allowedIps,
]);

return -1;
}

// Not submission file, allow normal access.
return NULL;
}

/**
* Check if an IP address is within a CIDR range.
*
* @param string $ip
* The IP address to check.
* @param string $cidr
* The CIDR range (e.g. "172.16.0.0/16").
*
* @return bool
* TRUE if the IP is within the CIDR range, FALSE otherwise.
*/
function os2forms_digital_signature_ip_in_cidr(string $ip, string $cidr): bool {
return IpUtils::checkIp($ip, $cidr);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider just using the IpUtils directly in the above code.

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,16 @@ services:
- '@config.factory'
- '@entity_type.manager'
- '@logger.channel.os2forms_digital_signature'
services:
logger.channel.os2forms_digital_signature:
parent: logger.channel_base
arguments: [ 'os2forms_digital_signature' ]

os2forms_digital_signature.signing_service:
class: Drupal\os2forms_digital_signature\Service\SigningService
arguments:
- '@http_client'
- '@datetime.time'
- '@config.factory'
- '@entity_type.manager'
- '@logger.channel.os2forms_digital_signature'
5 changes: 4 additions & 1 deletion modules/os2forms_digital_signature/src/Form/SettingsForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,27 @@ public function buildForm(array $form, FormStateInterface $form_state) {
$form['os2forms_digital_signature_remote_service_url'] = [
'#type' => 'textfield',
'#title' => $this->t('Signature server URL'),
'#required' => TRUE,
'#default_value' => $this->config(self::$configName)->get('os2forms_digital_signature_remote_service_url'),
'#description' => $this->t('E.g. https://signering.bellcom.dk/sign.php?'),
];
$form['os2forms_digital_signature_sign_hash_salt'] = [
'#type' => 'textfield',
'#title' => $this->t('Hash Salt used for signature'),
'#required' => TRUE,
'#default_value' => $this->config(self::$configName)->get('os2forms_digital_signature_sign_hash_salt'),
'#description' => $this->t('Must match hash salt on the signature server'),
];
$form['os2forms_digital_signature_submission_allowed_ips'] = [
'#type' => 'textfield',
'#title' => $this->t('List IPs which can download unsigned PDF submissions'),
'#default_value' => $this->config(self::$configName)->get('os2forms_digital_signature_submission_allowed_ips'),
'#description' => $this->t('Comma separated. e.g. 192.168.1.1,192.168.2.1'),
'#description' => $this->t('Comma separated. e.g. 192.168.1.1,192.168.2.1 or 172.16.0.0/16. If left empty no restrictions will be applied.'),
];
$form['os2forms_digital_signature_submission_retention_period'] = [
'#type' => 'textfield',
'#title' => $this->t('Unsigned submission timespan (s)'),
'#required' => TRUE,
'#default_value' => ($this->config(self::$configName)->get('os2forms_digital_signature_submission_retention_period')) ?? 300,
'#description' => $this->t('How many seconds can unsigned submission exist before being automatically deleted'),
];
Expand Down
Loading