Skip to content

Commit 9272158

Browse files
Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist
2 parents 024c122 + ebe1913 commit 9272158

File tree

18 files changed

+354
-39
lines changed

18 files changed

+354
-39
lines changed

app/Attachment.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,8 @@ public function getToken()
278278
/**
279279
* Outputs the current Attachment as download
280280
*/
281-
public function download($view = false)
281+
public function download($view = false, $headers = [])
282282
{
283-
$headers = [];
284283
// #533
285284
//return $this->getDisk()->download($this->getStorageFilePath(), \Str::ascii($this->file_name));
286285
if ($view) {

app/Console/Commands/FetchEmails.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,7 @@ public function processMessage($message, $message_id, $mailbox, $mailboxes, $ext
713713

714714
// Convert subject encoding
715715
if (preg_match('/=\?[a-z\d-]+\?[BQ]\?.*\?=/i', $subject)) {
716-
$subject = iconv_mime_decode($subject, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, 'UTF-8');
716+
$subject = \Helper::iconvMimeDecode($subject);
717717
}
718718

719719
$to = $this->formatEmailList($message->getTo());

app/Http/Controllers/CustomersController.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,8 @@ public function conversations($id)
264264
{
265265
$customer = Customer::findOrFail($id);
266266

267+
$this->checkLimitVisibility($customer);
268+
267269
$query = $customer->conversations()
268270
->where('customer_id', $customer->id)
269271
->whereIn('mailbox_id', auth()->user()->mailboxesIdsCanView())

app/Http/Controllers/OpenController.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,11 +189,22 @@ public function downloadAttachment($dir_1, $dir_2, $dir_3, $file_name, Request $
189189
break;
190190
}
191191
}
192+
if ($allowed_mime_type) {
193+
foreach (config('app.non_viewable_mime_types') as $mime_type) {
194+
if (preg_match('#'.$mime_type.'#', $attachment->mime_type)) {
195+
$allowed_mime_type = false;
196+
break;
197+
}
198+
}
199+
}
192200
if (!$allowed_mime_type) {
193201
$view_attachment = false;
194202
}
195203
}
196204

205+
// CSP header for exatra security.
206+
$csp_header_value = "script-src 'none'; frame-src 'none'; object-src 'none'; font-src 'none'; connect-src 'none'; media-src 'none'; frame-ancestors 'none'; form-action 'none'; base-uri 'none'; sandbox";
207+
197208
if (config('app.download_attachments_via') == 'apache') {
198209
// Send using Apache mod_xsendfile.
199210
$response = response(null)
@@ -202,6 +213,8 @@ public function downloadAttachment($dir_1, $dir_2, $dir_3, $file_name, Request $
202213

203214
if (!$view_attachment) {
204215
$response->header('Content-Disposition', 'attachment; filename="'.$attachment->file_name.'"');
216+
} else {
217+
$response->header('Content-Security-Policy', $csp_header_value);
205218
}
206219
} elseif (config('app.download_attachments_via') == 'nginx') {
207220
// Send using Nginx.
@@ -211,9 +224,15 @@ public function downloadAttachment($dir_1, $dir_2, $dir_3, $file_name, Request $
211224

212225
if (!$view_attachment) {
213226
$response->header('Content-Disposition', 'attachment; filename="'.$attachment->file_name.'"');
227+
} else {
228+
$response->header('Content-Security-Policy', $csp_header_value);
214229
}
215230
} else {
216-
$response = $attachment->download($view_attachment);
231+
$headers = [];
232+
if ($view_attachment) {
233+
$headers['Content-Security-Policy'] = $csp_header_value;
234+
}
235+
$response = $attachment->download($view_attachment, $headers);
217236
}
218237

219238
return $response;

app/Mailbox.php

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,22 @@ public static function getInProtocols()
670670
return \Eventy::filter('mailbox.in_protocols', self::$in_protocols);
671671
}
672672

673+
/**
674+
* Determines if Fetching settings for the mailbox have been saved by admin.
675+
*/
676+
public function inSettingsSaved()
677+
{
678+
return ($this->attributes['in_password'] !== null);
679+
}
680+
681+
/**
682+
* Determines if Sending settings for the mailbox have been saved by admin.
683+
*/
684+
public function outSettingsSaved()
685+
{
686+
return ($this->attributes['out_password'] !== null);
687+
}
688+
673689
/**
674690
* Get pivot table parameters for the user.
675691
*/
@@ -932,6 +948,15 @@ public function setMeta($key, $value)
932948

933949
public function setMetaParam($param, $value, $save = false)
934950
{
951+
// Encrypt some values.
952+
if ($param == 'oauth' && is_array($value)) {
953+
if (!empty($value['a_token'])) {
954+
$value['a_token'] = \Helper::encrypt($value['a_token']);
955+
}
956+
if (!empty($value['r_token'])) {
957+
$value['r_token'] = \Helper::encrypt($value['r_token']);
958+
}
959+
}
935960
$meta = $this->meta;
936961
$meta[$param] = $value;
937962
$this->meta = $meta;
@@ -976,7 +1001,14 @@ public function oauthEnabled()
9761001

9771002
public function oauthGetParam($param)
9781003
{
979-
return $this->meta['oauth'][$param] ?? '';
1004+
$value = $this->meta['oauth'][$param] ?? '';
1005+
1006+
// Decrypt some values.
1007+
if (in_array($param, ['a_token', 'r_token'])) {
1008+
$value = \Helper::decrypt($value);
1009+
}
1010+
1011+
return $value;
9801012
}
9811013

9821014
public function inOauthEnabled()

app/Misc/Helper.php

Lines changed: 129 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1629,6 +1629,10 @@ public static function sanitizeUploadedFileData($file_path, $storage, $content =
16291629
$content = $storage->get($file_path);
16301630
}
16311631
if ($content) {
1632+
// Remove comments from SVG content.
1633+
// https://github.com/freescout-help-desk/freescout/security/advisories/GHSA-cvr8-cw5c-5pfw
1634+
$content = preg_replace('/<!--(.|\s)*?-->/', '', $content);
1635+
16321636
$svg_sanitizer = new \enshrined\svgSanitize\Sanitizer();
16331637
$clean_content = $svg_sanitizer->sanitize($content);
16341638
if (!$clean_content) {
@@ -1755,6 +1759,10 @@ public static function disableSqlRequirePrimaryKey()
17551759
public static function downloadRemoteFileAsTmp($uri, $follow_redirects = true)
17561760
{
17571761
try {
1762+
// Sanitize URL first.
1763+
if (!self::sanitizeRemoteUrl($uri)) {
1764+
throw new \Exception('URL points to the local host', 1);
1765+
}
17581766
$contents = self::getRemoteFileContents($uri, $follow_redirects);
17591767

17601768
if (!$contents) {
@@ -1797,6 +1805,7 @@ public static function getRemoteFileContents($url, $follow_redirects = true)
17971805
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
17981806
if ($follow_redirects) {
17991807
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
1808+
curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
18001809
}
18011810
curl_setopt($ch, CURLOPT_URL, $url);
18021811
\Helper::setCurlDefaultOptions($ch);
@@ -1832,7 +1841,27 @@ public static function getRemoteFileContents($url, $follow_redirects = true)
18321841
}
18331842
}
18341843

1835-
public static function sanitizeRemoteUrl($url, $throw_exception = false)
1844+
public static function sanitizeRemoteUrl($url, $throw_exception = false, $follow_redirects = true)
1845+
{
1846+
if (!self::checkUrlIpAndHost($url, $throw_exception)) {
1847+
return '';
1848+
}
1849+
1850+
// Follow redirects and check final IP/host.
1851+
if ($follow_redirects) {
1852+
$last_redirected_url = self::curlGetLastRedirectedUrl($url);
1853+
1854+
if ($last_redirected_url != $url) {
1855+
if (!self::checkUrlIpAndHost($url, $throw_exception)) {
1856+
return '';
1857+
}
1858+
}
1859+
}
1860+
1861+
return $url;
1862+
}
1863+
1864+
public static function checkUrlIpAndHost($url, $throw_exception = false)
18361865
{
18371866
$parts = parse_url($url ?? '');
18381867

@@ -1854,7 +1883,15 @@ public static function sanitizeRemoteUrl($url, $throw_exception = false)
18541883
$hostname = gethostname();
18551884
$host_ip = gethostbyname($hostname);
18561885

1886+
// Can also include IP masks.
18571887
$restricted_hosts = [
1888+
'::1', // IPv6 loopback
1889+
'::ffff:127.0.0.1', // IPv4-mapped IPv6
1890+
'169.254.169.254', // AWS/GCP/Azure metadata
1891+
'fd00::/8', // IPv6 ULA
1892+
'10.0.0.0/8', // RFC1918
1893+
'172.16.0.0/12', // RFC1918
1894+
'fd00::/8', // RFC1918
18581895
'0.0.0.0',
18591896
'127.0.0.1',
18601897
'localhost',
@@ -1865,29 +1902,81 @@ public static function sanitizeRemoteUrl($url, $throw_exception = false)
18651902
$_SERVER['LOCAL_ADDR'] ?? '',
18661903
];
18671904

1868-
if (in_array($parts['host'], $restricted_hosts) && !in_array($parts['host'], $host_white_list)) {
1869-
if ($throw_exception) {
1870-
throw new \Exception(__('Domain or IP address is not allowed: :%host%. Whitelist it via APP_REMOTE_HOST_WHITE_LIST .env parameter.', ['%host%' => $parts['host']]), 1);
1871-
} else {
1872-
return '';
1905+
if (!in_array($parts['host'], $host_white_list)) {
1906+
if (in_array($parts['host'], $restricted_hosts) || self::checkIpByMask($parts['host'], $restricted_hosts)) {
1907+
if ($throw_exception) {
1908+
throw new \Exception(__('Domain or IP address is not allowed: :%host%. Whitelist it via APP_REMOTE_HOST_WHITE_LIST .env parameter.', ['%host%' => $parts['host']]), 1);
1909+
} else {
1910+
return '';
1911+
}
18731912
}
18741913
}
18751914

18761915
// Sanitize host IP address.
18771916
$remote_host_ip = gethostbyname($parts['host']);
1878-
if (in_array($remote_host_ip, ['0.0.0.0', '127.0.0.1', $host_ip, $_SERVER['SERVER_ADDR'] ?? '', $_SERVER['LOCAL_ADDR'] ?? ''])
1879-
&& !in_array($remote_host_ip, $host_white_list)
1880-
) {
1917+
if (!in_array($remote_host_ip, $host_white_list)) {
1918+
if (in_array($remote_host_ip, $restricted_hosts) || self::checkIpByMask($remote_host_ip, $restricted_hosts)) {
1919+
if ($throw_exception) {
1920+
throw new \Exception(__('Domain or IP address is not allowed: :%host%. Whitelist it via APP_REMOTE_HOST_WHITE_LIST .env parameter.', ['%host%' => $remote_host_ip]), 1);
1921+
} else {
1922+
return '';
1923+
}
1924+
}
1925+
}
1926+
1927+
return $url;
1928+
}
1929+
1930+
// Get last redicred URL.
1931+
public static function curlGetLastRedirectedUrl($url, $throw_exception = false)
1932+
{
1933+
$ch = curl_init();
1934+
1935+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
1936+
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
1937+
curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
1938+
1939+
curl_setopt($ch, CURLOPT_URL, $url);
1940+
// Get last effective URL.
1941+
1942+
\Helper::setCurlDefaultOptions($ch);
1943+
curl_setopt($ch, CURLOPT_TIMEOUT, 180);
1944+
curl_exec($ch);
1945+
1946+
$curl_errno = curl_errno($ch);
1947+
1948+
if ($curl_errno) {
18811949
if ($throw_exception) {
1882-
throw new \Exception(__('Domain or IP address is not allowed: :%host%. Whitelist it via APP_REMOTE_HOST_WHITE_LIST .env parameter.', ['%host%' => $remote_host_ip]), 1);
1950+
throw new \Exception('Could not check URL contents by following redirects: '.$curl_errno, 1);
18831951
} else {
18841952
return '';
18851953
}
18861954
}
18871955

1888-
return $url;
1956+
$last_redirected_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
1957+
if (PHP_VERSION_ID < 80000) {
1958+
\curl_close($ch);
1959+
}
1960+
1961+
return $last_redirected_url;
18891962
}
18901963

1964+
// Returns mask or false.
1965+
public static function checkIpByMask($ip, $masks = [])
1966+
{
1967+
if (!strstr($ip, '/')) {
1968+
return false;
1969+
}
1970+
foreach ($masks as $mask) {
1971+
if (!strstr($mask, '/')) {
1972+
continue;
1973+
}
1974+
if (\Symfony\Component\HttpFoundation\IpUtils::checkIp($ip, $mask)) {
1975+
return $mask;
1976+
}
1977+
}
1978+
return false;
1979+
}
18911980
public static function getTempDir()
18921981
{
18931982
return sys_get_temp_dir() ?: '/tmp';
@@ -2195,6 +2284,23 @@ public static function isConsole()
21952284
return app()->runningInConsole();
21962285
}
21972286

2287+
public static function isCron()
2288+
{
2289+
if (!self::isConsole()) {
2290+
return false;
2291+
}
2292+
if (php_sapi_name() == 'cli') {
2293+
if (isset($_SERVER['TERM'])) {
2294+
return false;
2295+
} else {
2296+
return true;
2297+
}
2298+
} else {
2299+
// The script was run from a webserver, or something else.
2300+
return false;
2301+
}
2302+
}
2303+
21982304
/**
21992305
* Show a warning when background jobs sending emails
22002306
* are not processed for some time.
@@ -2417,4 +2523,16 @@ public static function startsiWith($text, $string)
24172523
{
24182524
return (stripos($text, $string) === 0);
24192525
}
2526+
2527+
// The iconv_mime_decode() may throw an error even with ICONV_MIME_DECODE_CONTINUE_ON_ERROR.
2528+
// https://github.com/freescout-help-desk/freescout/issues/5265
2529+
public static function iconvMimeDecode($string, $mode = ICONV_MIME_DECODE_CONTINUE_ON_ERROR, $encoding = "UTF-8")
2530+
{
2531+
try {
2532+
return iconv_mime_decode($string, $mode, $encoding);
2533+
} catch (\Exception $e) {
2534+
self::logException($e);
2535+
return $string;
2536+
}
2537+
}
24202538
}

app/Misc/Mail.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -738,7 +738,7 @@ public static function imapUtf8($mime_encoded_text)
738738
if (function_exists('imap_utf8')) {
739739
return imap_utf8($mime_encoded_text);
740740
} else {
741-
return iconv_mime_decode($mime_encoded_text, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8");
741+
return \Helper::iconvMimeDecode($mime_encoded_text);
742742
}
743743
}
744744

@@ -1288,7 +1288,7 @@ public static function decodeSubject($subject)
12881288

12891289
// iconv_mime_decode() can't decode:
12901290
// =?iso-2022-jp?B?IBskQiFaSEcyPDpuQC4wTU1qIVs3Mkp2JSIlLyU3JSItahsoQg==?=
1291-
$subject_decoded = iconv_mime_decode($subject, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8");
1291+
$subject_decoded = \Helper::iconvMimeDecode($subject);
12921292

12931293
// Sometimes iconv_mime_decode() can't decode some parts of the subject:
12941294
// =?iso-2022-jp?B?IBskQiFaSEcyPDpuQC4wTU1qIVs3Mkp2JSIlLyU3JSItahsoQg==?=

app/Module.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,30 @@ public static function setLicense($alias, $license)
134134
$module->save();
135135
}
136136

137+
/**
138+
* Automatically encrypt license key.
139+
*/
140+
public function setLicenseAttribute($value)
141+
{
142+
if ($value != '') {
143+
$this->attributes['license'] = \Helper::encrypt($value);
144+
} else {
145+
$this->attributes['license'] = '';
146+
}
147+
}
148+
149+
/**
150+
* Automatically decrypt license key.
151+
*/
152+
public function getLicenseAttribute($value)
153+
{
154+
if (!$value) {
155+
return '';
156+
}
157+
158+
return \Helper::decrypt($value);
159+
}
160+
137161
public static function normalizeAlias($alias)
138162
{
139163
return trim(strtolower($alias));

0 commit comments

Comments
 (0)