@@ -57,21 +57,70 @@ function os2forms_digital_signature_file_download($uri) {
5757 $config = \Drupal::config(SettingsForm::$configName);
5858 $allowedIps = $config->get('os2forms_digital_signature_submission_allowed_ips');
5959
60- $allowedIpsArr = explode(',', $allowedIps);
61- $remoteIp = Drupal::request()->getClientIp();
60+ $allowedIpsArr = array_map('trim', explode(',', $allowedIps));
61+ // Remove empty entries (e.g. from trailing comma or empty config).
62+ $allowedIpsArr = array_filter($allowedIpsArr);
63+ $remoteIp = \Drupal::request()->getClientIp();
6264
63- // IP list is empty, or request IP is allowed .
64- if (empty($allowedIpsArr) || in_array($remoteIp, $allowedIpsArr) ) {
65+ // IP list is empty, allow access .
66+ if (empty($allowedIpsArr)) {
6567 $basename = basename($uri);
6668 return [
6769 'Content-disposition' => 'attachment; filename="' . $basename . '"',
6870 ];
6971 }
7072
71- // Otherwise - Deny access.
73+ // Check if remote IP matches any allowed IP or CIDR range.
74+ foreach ($allowedIpsArr as $allowedIp) {
75+ if ($remoteIp === $allowedIp || os2forms_digital_signature_ip_in_cidr($remoteIp, $allowedIp)) {
76+ $basename = basename($uri);
77+ return [
78+ 'Content-disposition' => 'attachment; filename="' . $basename . '"',
79+ ];
80+ }
81+ }
82+
83+ // Deny access and log warning.
84+ \Drupal::logger('os2forms_digital_signature')->warning('File download denied for IP @ip on URI @uri. Allowed IPs: @allowed', [
85+ '@ip' => $remoteIp,
86+ '@uri' => $uri,
87+ '@allowed' => $allowedIps,
88+ ]);
89+
7290 return -1;
7391 }
7492
7593 // Not submission file, allow normal access.
7694 return NULL;
7795}
96+
97+ /**
98+ * Check if an IP address is within a CIDR range.
99+ *
100+ * @param string $ip
101+ * The IP address to check.
102+ * @param string $cidr
103+ * The CIDR range (e.g. "172.16.0.0/16").
104+ *
105+ * @return bool
106+ * TRUE if the IP is within the CIDR range, FALSE otherwise.
107+ */
108+ function os2forms_digital_signature_ip_in_cidr(string $ip, string $cidr): bool {
109+ if (!str_contains($cidr, '/')) {
110+ return FALSE;
111+ }
112+
113+ [$subnet, $bits] = explode('/', $cidr, 2);
114+ $bits = (int) $bits;
115+
116+ $ip = ip2long($ip);
117+ $subnet = ip2long($subnet);
118+
119+ if ($ip === FALSE || $subnet === FALSE || $bits < 0 || $bits > 32) {
120+ return FALSE;
121+ }
122+
123+ $mask = -1 << (32 - $bits);
124+
125+ return ($ip & $mask) === ($subnet & $mask);
126+ }
0 commit comments