From ad73f9d8799a8d021256f5c66fcf0d1a5d207ccb Mon Sep 17 00:00:00 2001 From: w3houde Date: Sun, 19 Jul 2026 22:31:25 +0700 Subject: [PATCH 1/2] Fix #39020 avoid duplicate MO when BOM has a sub-BOM (#39163) * Fix #39020 avoid duplicate MO when BOM has a sub-BOM When generating child MOs from a BOM with a sub-BOM line, the lookup of the parent MO consume line used a substring filter (origin_id LIKE '%id%') that was not scoped to the parent MO. It could match an unrelated mrp_production row or none at all. When it matched none, the child MO was still created from the leftover parent POST data, producing a second MO identical to the parent instead of a real child. Scope the lookup to the parent MO with an exact match on origin_id and origin_type via customsql, and skip the iteration when no matching consume line is found so no duplicate parent MO can be created. Signed-off-by: Dolicraft * Fix #39020 use exact-match filter keys instead of customsql per review Add exact-match handling for fk_mo, origin_id and origin_type in MoLine::fetchAll and call it with those keys instead of a customsql clause, so the lookup stays valid on 18.0/19.0 where customsql would be the only alternative to the buggy LIKE. The universal-search filter form is handled separately on 20.0 and later. Signed-off-by: Dolicraft --------- Signed-off-by: Dolicraft Co-authored-by: Dolicraft --- htdocs/mrp/class/mo.class.php | 4 +++- htdocs/mrp/mo_card.php | 10 +++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/htdocs/mrp/class/mo.class.php b/htdocs/mrp/class/mo.class.php index b1cf985e4f3..d6eacf8b1a4 100644 --- a/htdocs/mrp/class/mo.class.php +++ b/htdocs/mrp/class/mo.class.php @@ -1923,8 +1923,10 @@ class MoLine extends CommonObjectLine $sqlwhere = array(); if (count($filter) > 0) { foreach ($filter as $key => $value) { - if ($key == 't.rowid') { + if ($key == 't.rowid' || $key == 'fk_mo' || $key == 'origin_id') { $sqlwhere[] = $key." = ".((int) $value); + } elseif ($key == 'origin_type') { + $sqlwhere[] = $key." = '".$this->db->escape($value)."'"; } elseif (strpos($key, 'date') !== false) { $sqlwhere[] = $key." = '".$this->db->idate($value)."'"; } elseif ($key == 'customsql') { diff --git a/htdocs/mrp/mo_card.php b/htdocs/mrp/mo_card.php index 6d66e835174..f71afd103ed 100644 --- a/htdocs/mrp/mo_card.php +++ b/htdocs/mrp/mo_card.php @@ -154,7 +154,15 @@ if (empty($reshook)) { $objectbomchildline->fetch($id_bom_line); - $TMoLines = $moline->fetchAll('DESC', 'rowid', '1', '', array('origin_id' => $id_bom_line)); + // Find the consume line of the parent MO generated from this BOM line. + // The lookup must be scoped to the parent MO and use an exact match on origin_id/origin_type, + // otherwise the default 'origin_id LIKE %..%' filter can return an unrelated line (or none), + // which would let the child MO be created from the leftover parent POST data (duplicate MO). + $TMoLines = $moline->fetchAll('DESC', 'rowid', '1', '', array('fk_mo' => $mo_parent->id, 'origin_id' => $id_bom_line, 'origin_type' => 'bomline')); + + if (empty($TMoLines)) { + continue; + } foreach ($TMoLines as $tmpmoline) { $_POST['fk_bom'] = $objectbomchildline->fk_bom_child; From 9926334e78e2e609381f3f5bc3f58348f4986f6c Mon Sep 17 00:00:00 2001 From: ThomasNgr-OpenDSI Date: Thu, 23 Jul 2026 09:21:33 +0200 Subject: [PATCH 2/2] Sec: sql injection via api in POST of thirdparties/id/banckaccount - (#39234) reported by MDnyn Co-authored-by: Laurent Destailleur --- htdocs/accountancy/admin/productaccount.php | 2 +- htdocs/admin/external_rss.php | 2 +- htdocs/contrat/class/contrat.class.php | 2 +- htdocs/core/class/discount.class.php | 2 +- htdocs/user/class/userbankaccount.class.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/htdocs/accountancy/admin/productaccount.php b/htdocs/accountancy/admin/productaccount.php index a524f29b840..e35008ab2d2 100644 --- a/htdocs/accountancy/admin/productaccount.php +++ b/htdocs/accountancy/admin/productaccount.php @@ -326,7 +326,7 @@ if (!empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) { $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "accounting_account as aa ON aa.account_number = p." . $accountancy_field_name . " AND aa.fk_pcg_version = '" . $db->escape($pcgvercode) . "'"; } if (!empty($searchCategoryProductList)) { - $sql .= ' LEFT JOIN '.MAIN_DB_PREFIX."categorie_product as cp ON p.rowid = cp.fk_product"; // We'll need this table joined to the select in order to filter by categ + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."categorie_product as cp ON p.rowid = cp.fk_product"; // We'll need this table joined to the select in order to filter by categ } $sql .= ' WHERE p.entity IN ('.getEntity('product').')'; if (strlen(trim($search_current_account))) { diff --git a/htdocs/admin/external_rss.php b/htdocs/admin/external_rss.php index dca0d5c02f7..c5ba28d4ef8 100644 --- a/htdocs/admin/external_rss.php +++ b/htdocs/admin/external_rss.php @@ -136,7 +136,7 @@ if (GETPOST("delete")) { $obj = $db->fetch_object($resql); $sql = "DELETE FROM ".MAIN_DB_PREFIX."boxes"; - $sql .= " WHERE entity = ".$conf->entity; + $sql .= " WHERE entity = ".((int) $conf->entity); $sql .= " AND box_id = ".((int) $obj->rowid); $resql = $db->query($sql); diff --git a/htdocs/contrat/class/contrat.class.php b/htdocs/contrat/class/contrat.class.php index a29a632754c..9bafe098a09 100644 --- a/htdocs/contrat/class/contrat.class.php +++ b/htdocs/contrat/class/contrat.class.php @@ -3615,7 +3615,7 @@ class ContratLigne extends CommonObjectLine if ($this->date_end > 0) { $sql .= ",date_fin_validite"; } - $sql .= ") VALUES ($this->fk_contrat, '', '".$this->db->escape($this->description)."',"; + $sql .= ") VALUES (".((int) $this->fk_contrat).", '', '".$this->db->escape($this->description)."',"; $sql .= ($this->fk_product > 0 ? $this->fk_product : "null").","; $sql .= " '".$this->db->escape($this->qty)."',"; $sql .= " '".$this->db->escape($this->vat_src_code)."',"; diff --git a/htdocs/core/class/discount.class.php b/htdocs/core/class/discount.class.php index 49d43ce4a28..bd4df66e1ff 100644 --- a/htdocs/core/class/discount.class.php +++ b/htdocs/core/class/discount.class.php @@ -275,7 +275,7 @@ class DiscountAbsolute extends CommonObject $sql .= " multicurrency_amount_ht, multicurrency_amount_tva, multicurrency_amount_ttc,"; $sql .= " fk_facture_source, fk_invoice_supplier_source, multicurrency_code, multicurrency_tx"; $sql .= ")"; - $sql .= " VALUES (".$conf->entity.", '".$this->db->idate($this->datec != '' ? $this->datec : dol_now())."', ".((int) $this->fk_soc).", ".(empty($this->discount_type) ? 0 : intval($this->discount_type)).", ".((int) $userid).", '".$this->db->escape($this->description)."',"; + $sql .= " VALUES (".((int) $conf->entity).", '".$this->db->idate($this->datec != '' ? $this->datec : dol_now())."', ".((int) $this->fk_soc).", ".(empty($this->discount_type) ? 0 : intval($this->discount_type)).", ".((int) $userid).", '".$this->db->escape($this->description)."',"; $sql .= " ".price2num($this->amount_ht).", ".price2num($this->amount_tva).", ".price2num($this->amount_ttc).", ".price2num($this->tva_tx).", '".$this->db->escape($this->vat_src_code)."',"; $sql .= " ".price2num($this->multicurrency_amount_ht).", ".price2num($this->multicurrency_amount_tva).", ".price2num($this->multicurrency_amount_ttc).", "; $sql .= " ".($this->fk_facture_source ? ((int) $this->fk_facture_source) : "null").","; diff --git a/htdocs/user/class/userbankaccount.class.php b/htdocs/user/class/userbankaccount.class.php index 85c8cbc7af7..e32ed6909e5 100644 --- a/htdocs/user/class/userbankaccount.class.php +++ b/htdocs/user/class/userbankaccount.class.php @@ -94,7 +94,7 @@ class UserBankAccount extends Account $now = dol_now(); $sql = "INSERT INTO ".$this->db->prefix()."user_rib (fk_user, datec)"; - $sql .= " VALUES (".$this->userid.", '".$this->db->idate($now)."')"; + $sql .= " VALUES (".((int) $this->userid).", '".$this->db->idate($now)."')"; $resql = $this->db->query($sql); if ($resql) { if ($this->db->affected_rows($resql)) {