| @@ -66,10 +66,19 @@ | ||
| 66 | 66 | public function createBlockAction($blockId = null, $blockType = null, $pinPoint = null, $viewName = null) { |
| 67 | 67 | $response = array(); |
| 68 | 68 | // If viewName not provided read it from request vars. |
| 69 | 69 | if (!$viewName) { |
| 70 | - $viewName = filter_input(INPUT_GET, 'viewName', FILTER_SANITIZE_STRING); | |
| 70 | + $viewName = filter_input(INPUT_GET, 'viewName', FILTER_UNSAFE_RAW); | |
| 71 | + $viewName = is_string($viewName) ? sanitize_text_field($viewName) : null; | |
| 71 | 72 | } |
| 73 | + // $viewName is interpolated into a filesystem path by CJTController::getView(), | |
| 74 | + // which require_once's "views/blocks/{$viewName}/view.php". Restrict it to the | |
| 75 | + // known block views so traversal sequences can never reach that include. | |
| 76 | + $allowedBlockViews = array('cjt-block', 'block', 'metabox', 'create-metabox'); | |
| 77 | + if ($viewName && !in_array($viewName, $allowedBlockViews, true)) { | |
| 78 | + $this->httpCode = '403 Forbidden'; | |
| 79 | + return; | |
| 80 | + } | |
| 72 | 81 | // Prepare parameters. |
| 73 | 82 | $defaultBlockName = 'block_' . hexdec(substr(md5(time()), 0, 6)); |
| 74 | 83 | $wordpressMYSQLTime = current_time('mysql'); |
| 75 | 84 | // Block data to insert. |
| @@ -90,8 +99,10 @@ | ||
| 90 | 99 | if (array_key_exists($name, $_GET)) { |
| 91 | 100 | $blockData[$name] = $_GET[$name]; |
| 92 | 101 | } |
| 93 | 102 | } |
| 103 | + // Harden the block name server-side (CVE-2025-13533 defence in depth). | |
| 104 | + $blockData['name'] = self::sanitizeBlockName($blockData['name']); | |
| 94 | 105 | // Import block model. |
| 95 | 106 | require_once CJTOOLBOX_MODELS_PATH . '/block.php'; |
| 96 | 107 | $block = new CJTBlockModel($blockData); |
| 97 | 108 | // Add block. |
| @@ -139,9 +150,10 @@ | ||
| 139 | 150 | $allowedViews = array( |
| 140 | 151 | 'blocks/new' => array(), |
| 141 | 152 | ); |
| 142 | 153 | // Prepare parameters. |
| 143 | - $viewName = filter_input(INPUT_GET, 'viewName', FILTER_SANITIZE_STRING); | |
| 154 | + $viewName = filter_input(INPUT_GET, 'viewName', FILTER_UNSAFE_RAW); | |
| 155 | + $viewName = is_string($viewName) ? sanitize_text_field($viewName) : ''; | |
| 144 | 156 | if (array_key_exists($viewName, $allowedViews) === FALSE) { |
| 145 | 157 | $this->httpCode = '403 Forbidden'; |
| 146 | 158 | } |
| 147 | 159 | else { |
| @@ -204,8 +216,13 @@ | ||
| 204 | 216 | // Push block id into block data. |
| 205 | 217 | $blockData = ( object ) $postedblockPartialData; |
| 206 | 218 | $blockData->id = $id; |
| 207 | 219 | |
| 220 | + // Harden the block name server-side (CVE-2025-13533 defence in depth). | |
| 221 | + if ( isset( $blockData->name ) ) { | |
| 222 | + $blockData->name = self::sanitizeBlockName( $blockData->name ); | |
| 223 | + } | |
| 224 | + | |
| 208 | 225 | // Recalculate pinPoint field value. |
| 209 | 226 | ! $calculatePinPoint or ( CJTBlockModel::arrangePins( $blockData ) && CJTBlockModel::calculateBlockPinPoint( $blockData ) ); |
| 210 | 227 | |
| 211 | 228 | // Create block revision. |
| @@ -251,7 +268,32 @@ | ||
| 251 | 268 | $order = array('normal' => $_GET['order']); |
| 252 | 269 | // Centralized orders to be shared between all users! |
| 253 | 270 | $this->model->setOrder($order); |
| 254 | 271 | $this->response = array('order' => $order, 'state' => 'saved'); |
| 272 | + } | |
| 273 | + | |
| 274 | + /** | |
| 275 | + * Sanitize a code-block name coming from the request. | |
| 276 | + * | |
| 277 | + * Defence-in-depth for the stored-XSS issue (CVE-2025-13533): the client-side UI | |
| 278 | + * already restricts block names to A-Z, 0-9, space, '-' and '_', but that check is | |
| 279 | + * trivially bypassed (e.g. via an HTTP proxy). We enforce the exact same contract | |
| 280 | + * server-side so characters that could break out of an HTML attribute/element - | |
| 281 | + * such as < > " ' - can never be persisted. Output is still escaped at every sink | |
| 282 | + * as the primary defence; this simply keeps the stored data clean. | |
| 283 | + * | |
| 284 | + * @param mixed $name Raw name value from the request. | |
| 285 | + * @return string Sanitized name (never empty). | |
| 286 | + */ | |
| 287 | + public static function sanitizeBlockName($name) { | |
| 288 | + // Keep only the documented allowed characters. | |
| 289 | + $name = preg_replace('/[^A-Za-z0-9 _-]/', '', (string) $name); | |
| 290 | + // Collapse to a trimmed value and guard against an empty result. | |
| 291 | + $name = trim($name); | |
| 292 | + if ($name === '') { | |
| 293 | + $name = 'block_' . hexdec(substr(md5((string) time()), 0, 6)); | |
| 294 | + } | |
| 295 | + // Respect the 50 char column/UI limit. | |
| 296 | + return substr($name, 0, 50); | |
| 255 | 297 | } |
| 256 | 298 | |
| 257 | 299 | } // End class. |