Skip to content

Commit 8a4cf2f

Browse files
committed
Bug fixes to secure against Server-Side Request Forgery (SSRF) vulnerabilities
1 parent 0b9066b commit 8a4cf2f

17 files changed

Lines changed: 175 additions & 97 deletions

File tree

plugins/api/Admin.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,15 @@ public function postKirimWA()
266266
}
267267

268268
$url = $waapiserver."/wagateway/kirimpesan";
269+
if (!filter_var($url, FILTER_VALIDATE_URL) || !preg_match('/^https:\/\//i', $url)) {
270+
echo json_encode(['status' => false, 'msg' => 'Invalid or insecure WA Gateway URL']);
271+
exit();
272+
}
269273
$curlHandle = curl_init();
270274
curl_setopt($curlHandle, CURLOPT_URL, $url);
275+
curl_setopt($curlHandle, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
276+
curl_setopt($curlHandle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS);
277+
curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, false);
271278
curl_setopt($curlHandle, CURLOPT_POSTFIELDS, http_build_query([
272279
'type' => 'text',
273280
'sender' => $waapiphonenumber,
@@ -279,7 +286,7 @@ public function postKirimWA()
279286
curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1);
280287
curl_setopt($curlHandle, CURLOPT_TIMEOUT, 30);
281288
curl_setopt($curlHandle, CURLOPT_POST, 1);
282-
curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, false);
289+
curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, true);
283290

284291
$response = curl_exec($curlHandle);
285292

@@ -316,14 +323,21 @@ public function postKirimWAMedia()
316323
$waapiphonenumber = $this->settings->get('wagateway.phonenumber');
317324
$waapiserver = $this->settings->get('wagateway.server');
318325
$url = $waapiserver."/wagateway/kirimgambar";
326+
if (!filter_var($url, FILTER_VALIDATE_URL) || !preg_match('/^https:\/\//i', $url)) {
327+
echo json_encode(['status' => false, 'msg' => 'Invalid or insecure WA Gateway URL']);
328+
exit();
329+
}
319330
$curlHandle = curl_init();
320331
curl_setopt($curlHandle, CURLOPT_URL, $url);
332+
curl_setopt($curlHandle, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
333+
curl_setopt($curlHandle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS);
334+
curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, false);
321335
curl_setopt($curlHandle, CURLOPT_POSTFIELDS,"type=image&sender=".$waapiphonenumber."&number=".$_POST['number']."&message=".$_POST['message']."&url=".$_POST['file']."&api_key=".$waapitoken);
322336
curl_setopt($curlHandle, CURLOPT_HEADER, 0);
323337
curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1);
324338
curl_setopt($curlHandle, CURLOPT_TIMEOUT,30);
325339
curl_setopt($curlHandle, CURLOPT_POST, 1);
326-
curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, false);
340+
curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, true);
327341
$response = curl_exec($curlHandle);
328342
curl_close($curlHandle);
329343
echo $response;
@@ -336,14 +350,21 @@ public function postKirimWADocument()
336350
$waapiphonenumber = $this->settings->get('wagateway.phonenumber');
337351
$waapiserver = $this->settings->get('wagateway.server');
338352
$url = $waapiserver."/wagateway/kirimfile";
353+
if (!filter_var($url, FILTER_VALIDATE_URL) || !preg_match('/^https:\/\//i', $url)) {
354+
echo json_encode(['status' => false, 'msg' => 'Invalid or insecure WA Gateway URL']);
355+
exit();
356+
}
339357
$curlHandle = curl_init();
340358
curl_setopt($curlHandle, CURLOPT_URL, $url);
359+
curl_setopt($curlHandle, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
360+
curl_setopt($curlHandle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS);
361+
curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, false);
341362
curl_setopt($curlHandle, CURLOPT_POSTFIELDS,"type=document&sender=".$waapiphonenumber."&number=".$_POST['number']."&message=".$_POST['message']."&url=".$_POST['file']."&api_key=".$waapitoken);
342363
curl_setopt($curlHandle, CURLOPT_HEADER, 0);
343364
curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1);
344365
curl_setopt($curlHandle, CURLOPT_TIMEOUT,30);
345366
curl_setopt($curlHandle, CURLOPT_POST, 1);
346-
curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, false);
367+
curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, true);
347368
$response = curl_exec($curlHandle);
348369
curl_close($curlHandle);
349370
echo $response;

plugins/api/Site.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,7 @@ public function getApam()
567567
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
568568
curl_setopt($ch, CURLOPT_TIMEOUT,30);
569569
curl_setopt($ch, CURLOPT_POST, 1);
570-
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
570+
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
571571
$response = curl_exec($ch);
572572
curl_close($ch);
573573
}
@@ -999,7 +999,7 @@ public function getApam()
999999
'Content-Type: application/json',
10001000
'Content-Length: ' . strlen($params_string))
10011001
);
1002-
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
1002+
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
10031003

10041004
//execute post
10051005
$request = curl_exec($ch);
@@ -1032,7 +1032,7 @@ public function getApam()
10321032
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
10331033
curl_setopt($ch, CURLOPT_TIMEOUT,30);
10341034
curl_setopt($ch, CURLOPT_POST, 1);
1035-
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
1035+
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
10361036
$response = curl_exec($ch);
10371037
curl_close($ch);
10381038
}

plugins/farmasi/Admin.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@ public function postDetailpemberianobatData()
555555
$totalRecordwithFilter = $records['allcount'];
556556

557557
## Fetch records
558-
$sel = $this->db()->pdo()->prepare("select * from detail_pemberian_obat WHERE 1 ".$searchQuery." order by ".$columnName." ".$columnSortOrder." limit ".intval($row1).",".intval($rowperpage));
558+
$sel = $this->db()->pdo()->prepare("select * from detail_pemberian_obat WHERE 1 ".$searchQuery." order by ".$columnName." ".$columnSortOrder." limit ".(int)$row1.",".(int)$rowperpage);
559559
$sel->execute($params);
560560
$result = $sel->fetchAll(\PDO::FETCH_ASSOC);
561561

@@ -668,7 +668,7 @@ public function postRiwayatbarangmedisData()
668668
$totalRecordwithFilter = $records['allcount'];
669669

670670
## Fetch records
671-
$sel = $this->db()->pdo()->prepare("select * from riwayat_barang_medis WHERE 1 ".$searchQuery." order by ".$columnName." ".$columnSortOrder." limit ".intval($row1).",".intval($rowperpage));
671+
$sel = $this->db()->pdo()->prepare("select * from riwayat_barang_medis WHERE 1 ".$searchQuery." order by ".$columnName." ".$columnSortOrder." limit ".(int)$row1.",".(int)$rowperpage);
672672
$sel->execute($params);
673673
$result = $sel->fetchAll(\PDO::FETCH_ASSOC);
674674

plugins/igd/Admin.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1731,7 +1731,11 @@ public function postTriaseIgdSave()
17311731
// Build INSERT query manually to avoid framework interference
17321732
$fields = array_keys($data_to_save);
17331733
$placeholders = ':' . implode(', :', $fields);
1734-
$sql = "INSERT INTO mlite_triase_igd (" . implode(', ', array_map(function($f){return "`$f`";}, $fields)) . ") VALUES (" . $placeholders . ")";
1734+
$escaped_fields = [];
1735+
foreach ($fields as $f) {
1736+
$escaped_fields[] = "`$f`";
1737+
}
1738+
$sql = "INSERT INTO mlite_triase_igd (" . implode(', ', $escaped_fields) . ") VALUES (" . $placeholders . ")";
17351739

17361740
error_log('DEBUG: Direct SQL: ' . $sql);
17371741
error_log('DEBUG: SQL Data: ' . print_r($data_to_save, true));

plugins/master/src/JnsPerawatanLab.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ public function postData()
184184
$totalRecords = $records['allcount'];
185185

186186
$stmt = $this->db()->pdo()->prepare("SELECT COUNT(*) AS allcount FROM jns_perawatan_lab WHERE 1=1 $searchQuery");
187-
if (!empty($search_text)) {
187+
if (!empty($search_text) && in_array($search_field, $allowedColumns)) {
188188
$stmt->bindValue(':search_text', "%$search_text%", \PDO::PARAM_STR);
189189
}
190190
$stmt->execute();

plugins/master/src/Poliklinik.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,8 @@ public function postData()
165165
$totalRecordwithFilter = $records['allcount'];
166166

167167
// Data paginated
168-
$sql = "SELECT * FROM poliklinik WHERE 1=1 $searchQuery ORDER BY `$columnName` $columnSortOrder LIMIT :row1, :rowperpage";
168+
$sql = "SELECT * FROM poliklinik WHERE 1=1 $searchQuery ORDER BY `$columnName` $columnSortOrder LIMIT ".(int)$row1.", ".(int)$rowperpage;
169169
$stmt = $this->core->db()->pdo()->prepare($sql);
170-
$stmt->bindValue(':row1', intval($row1), \PDO::PARAM_INT);
171-
$stmt->bindValue(':rowperpage', intval($rowperpage), \PDO::PARAM_INT);
172170
foreach ($params as $key => $val) {
173171
$stmt->bindValue($key, $val);
174172
}
@@ -329,11 +327,10 @@ public function getChart($type = '', $column = '')
329327
$datasets = json_encode(array_column($datasets, "COUNT($column)"));
330328
}
331329

332-
$database = DBNAME;
333330
$nama_table = 'poliklinik';
334331

335-
$stmt = $this->core->db()->pdo()->prepare("SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=? AND TABLE_NAME=?");
336-
$stmt->execute([$database, $nama_table]);
332+
$stmt = $this->core->db()->pdo()->prepare("SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=?");
333+
$stmt->execute([$nama_table]);
337334
$result = $stmt->fetchAll();
338335

339336
return [

plugins/mlite_api_key/Admin.php

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,10 @@ public function postData()
5858
}
5959
$stmt->execute();
6060
$records = $stmt->fetch();
61-
$totalRecordwithFilter = $records['allcount'];
61+
$totalRecordwithFilter = $records['allcount'] ?? 0;
6262

63-
$sql = "SELECT * FROM mlite_api_key WHERE 1=1 $searchQuery ORDER BY `$columnName` $columnSortOrder LIMIT :row1, :rowperpage";
63+
$sql = "SELECT * FROM mlite_api_key WHERE 1=1 $searchQuery ORDER BY `$columnName` $columnSortOrder LIMIT ".(int)$row1.", ".(int)$rowperpage;
6464
$stmt = $this->db()->pdo()->prepare($sql);
65-
$stmt->bindValue(':row1', intval($row1), \PDO::PARAM_INT);
66-
$stmt->bindValue(':rowperpage', intval($rowperpage), \PDO::PARAM_INT);
6765
if (!empty($search_text) && in_array($search_field, $allowedColumns)) {
6866
$stmt->bindValue(':search_text', "%$search_text%", \PDO::PARAM_STR);
6967
}
@@ -235,8 +233,8 @@ public function getChart($type = '', $column = '')
235233
$database = DBNAME;
236234
$nama_table = 'mlite_api_key';
237235

238-
$stmt = $this->db()->pdo()->prepare("SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=? AND TABLE_NAME=?");
239-
$stmt->execute([$database, $nama_table]);
236+
$stmt = $this->db()->pdo()->prepare("SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=?");
237+
$stmt->execute([$nama_table]);
240238
$result = $stmt->fetchAll();
241239

242240
echo $this->draw('chart.html', ['type' => $type, 'column' => htmlspecialchars_array($result), 'labels' => $labels, 'datasets' => $datasets, 'slug' => $slug]);

plugins/mlite_logs/Admin.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,10 @@ public function postData()
5555
}
5656
$stmt->execute();
5757
$records = $stmt->fetch();
58-
$totalRecordwithFilter = $records['allcount'];
58+
$totalRecordwithFilter = $records['allcount'] ?? 0;
5959

60-
$sql = "SELECT * FROM mlite_query_logs WHERE 1=1 $searchQuery ORDER BY `$columnName` $columnSortOrder LIMIT :row1, :rowperpage";
60+
$sql = "SELECT * FROM mlite_query_logs WHERE 1=1 $searchQuery ORDER BY `$columnName` $columnSortOrder LIMIT ".(int)$row1.", ".(int)$rowperpage;
6161
$stmt = $this->db()->pdo()->prepare($sql);
62-
$stmt->bindValue(':row1', intval($row1), \PDO::PARAM_INT);
63-
$stmt->bindValue(':rowperpage', intval($rowperpage), \PDO::PARAM_INT);
6462
if (!empty($search_text) && in_array($search_field, $allowedColumns)) {
6563
$stmt->bindValue(':search_text', "%$search_text%", \PDO::PARAM_STR);
6664
}

plugins/orthanc/Admin.php

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,21 @@ public function postTestOpenai()
5858
}
5959

6060
if($result) {
61+
// SSRF protection: validate ai_api_url is a public https URL
62+
$url_parts = parse_url($orthanc['ai_api_url']);
63+
if (!filter_var($orthanc['ai_api_url'], FILTER_VALIDATE_URL) ||
64+
!isset($url_parts['scheme']) ||
65+
strtolower($url_parts['scheme']) !== 'https') {
66+
$output = array('message' => 'error', 'code' => '400', 'desc' => 'Invalid or insecure API URL');
67+
echo json_encode(htmlspecialchars_array($output));
68+
exit();
69+
}
70+
6171
$curl = curl_init();
6272
curl_setopt ($curl, CURLOPT_URL, $orthanc['ai_api_url']);
73+
curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
74+
curl_setopt($curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS);
75+
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, false);
6376
curl_setopt($curl, CURLOPT_HTTPHEADER, array(
6477
'Content-Type: application/json',
6578
'Authorization: Bearer ' . $orthanc['ai_api_key']
@@ -221,20 +234,15 @@ public function postSavePACS()
221234
// --- Validasi URL ---
222235
$valid = false;
223236
if (filter_var($url, FILTER_VALIDATE_URL)) {
224-
$valid = true;
225-
} else {
226-
// Fallback validasi Docker container host
227237
$parts = parse_url($url);
228-
if (
229-
$parts &&
230-
!empty($parts['scheme']) &&
231-
in_array($parts['scheme'], ['http','https'], true) &&
232-
!empty($parts['host']) &&
233-
(
234-
preg_match('/^[a-zA-Z0-9\-_]+$/', $parts['host']) ||
235-
filter_var($parts['host'], FILTER_VALIDATE_IP)
236-
)
237-
) {
238+
// Require HTTP/HTTPS, but strictly validate host to prevent SSRF if this is external
239+
// Assuming orthanc server is the only allowed host
240+
$orthanc_server = $this->settings->get('orthanc.server');
241+
$orthanc_parts = parse_url($orthanc_server);
242+
243+
if (isset($parts['scheme']) && in_array(strtolower($parts['scheme']), ['http','https']) &&
244+
isset($parts['host']) && isset($orthanc_parts['host']) &&
245+
strtolower($parts['host']) === strtolower($orthanc_parts['host'])) {
238246
$valid = true;
239247
}
240248
}
@@ -670,7 +678,7 @@ public function postTestConnection()
670678
curl_setopt($curl, CURLOPT_USERPWD, $orthanc['username'].":".$orthanc['password']);
671679
curl_setopt($curl, CURLOPT_TIMEOUT, 10);
672680
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5);
673-
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
681+
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
674682
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
675683

676684
$resp = curl_exec($curl);
@@ -801,7 +809,7 @@ private function setupCurl($url, $orthanc, $options = [])
801809
curl_setopt($curl, CURLOPT_USERPWD, $orthanc['username'].":".$orthanc['password']);
802810
curl_setopt($curl, CURLOPT_TIMEOUT, 30);
803811
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10);
804-
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
812+
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
805813
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
806814

807815
// Apply additional options
@@ -1203,7 +1211,7 @@ private function downloadSeriesStream($seriesId, $orthanc)
12031211
curl_setopt($curl, CURLOPT_FILE, $fp);
12041212
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
12051213
curl_setopt($curl, CURLOPT_TIMEOUT, 300);
1206-
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
1214+
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
12071215
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
12081216

12091217
$result = curl_exec($curl);
@@ -1891,7 +1899,7 @@ public function postAiinterpretasi()
18911899
'Content-Type: application/json',
18921900
'Authorization: Bearer ' . $apiKey
18931901
]);
1894-
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
1902+
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
18951903
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
18961904

18971905
$response = curl_exec($ch);

plugins/pasien/Admin.php

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -498,17 +498,31 @@ public function postHapusBerkasDigital()
498498

499499
public function getDownloadBerkasDigital()
500500
{
501-
$file = explode('/', $_GET['lokasi_file']);
502-
$file_name = $file['2'];
503-
$file_url = WEBAPPS_URL.'/berkasrawat/' . $_GET['lokasi_file'];
501+
$file_path_param = $_GET['lokasi_file'];
502+
503+
// Basic validation to prevent directory traversal
504+
if (strpos($file_path_param, '..') !== false || strpos($file_path_param, '/') !== false || strpos($file_path_param, '\\') !== false) {
505+
echo 'Akses ditolak.';
506+
exit();
507+
}
508+
509+
$file_name = $file_path_param;
510+
$file_url = WEBAPPS_URL.'/berkasrawat/' . $file_name;
511+
512+
// Validate that the file exists and is within the allowed directory
513+
$real_path = realpath(UPLOADS.'/berkasrawat/' . $file_name);
514+
if ($real_path === false || strpos($real_path, realpath(UPLOADS.'/berkasrawat/')) !== 0) {
515+
echo 'File tidak ditemukan.';
516+
exit();
517+
}
504518

505519
// Configure.
506520
header('Content-Type: application/octet-stream');
507521
header("Content-Transfer-Encoding: Binary");
508522
header("Content-disposition: attachment; filename=\"".$file_name."\"");
509523

510524
// Actual download.
511-
readfile($file_url);
525+
readfile($real_path);
512526

513527
exit();
514528
}

0 commit comments

Comments
 (0)