/** * Transliterates a string in given language. Various postprocessing possible. * * @param \Symfony\Component\HttpFoundation\Request $request * The input string and language for the transliteration. * Optionally may contain the replace_pattern, replace, lowercase parameters. * * @return \Symfony\Component\HttpFoundation\JsonResponse * The transliterated string. */ public function transliterate(Request $request) { $text = $request->query->get('text'); $langcode = $request->query->get('langcode'); $replace_pattern = $request->query->get('replace_pattern'); $replace_token = $request->query->get('replace_token'); $replace = $request->query->get('replace'); $lowercase = $request->query->get('lowercase'); $transliterated = $this->transliteration->transliterate($text, $langcode, '_'); if ($lowercase) { $transliterated = Unicode::strtolower($transliterated); } if (isset($replace_pattern) && isset($replace)) { if (!isset($replace_token)) { throw new AccessDeniedException("Missing 'replace_token' query parameter."); } elseif (!$this->tokenGenerator->validate($replace_token, $replace_pattern)) { throw new AccessDeniedException("Invalid 'replace_token' query parameter."); } // Quote the pattern delimiter and remove null characters to avoid the e // or other modifiers being injected. $transliterated = preg_replace('@' . strtr($replace_pattern, ['@' => '\@', chr(0) => '']) . '@', $replace, $transliterated); } return new JsonResponse($transliterated); }
/** * {@inheritdoc} */ public function validateForm($form_id, &$form, &$form_state) { // If this form is flagged to always validate, ensure that previous runs of // validation are ignored. if (!empty($form_state['must_validate'])) { $form_state['validation_complete'] = FALSE; } // If this form has completed validation, do not validate again. if (!empty($form_state['validation_complete'])) { return; } // If the session token was set by self::prepareForm(), ensure that it // matches the current user's session. if (isset($form['#token'])) { if (!$this->csrfToken->validate($form_state['values']['form_token'], $form['#token'])) { $url = $this->requestStack->getCurrentRequest()->getRequestUri(); // Setting this error will cause the form to fail validation. $this->setErrorByName('form_token', $form_state, $this->t('The form has become outdated. Copy any unsaved work in the form below and then <a href="@link">reload this page</a>.', array('@link' => $url))); // Stop here and don't run any further validation handlers, because they // could invoke non-safe operations which opens the door for CSRF // vulnerabilities. $this->finalizeValidation($form, $form_state, $form_id); return; } } // Recursively validate each form element. $this->doValidateForm($form, $form_state, $form_id); $this->finalizeValidation($form, $form_state, $form_id); $this->handleErrorsWithLimitedValidation($form, $form_state, $form_id); }
/** * {@inheritdoc} */ public function access(Route $route, Request $request, AccountInterface $account) { // Suppress notices when on other pages when menu system still checks access. $name = $request->attributes->get('name'); $token = $request->query->get('token'); $destination = $request->query->get('destination'); return $account->hasPermission('switch users') && $this->csrfToken->validate($token, "devel/switch/{$name}|" . $destination, TRUE) ? static::ALLOW : static::DENY; }
protected function setUp() { parent::setUp(); // Create the machine name controller. $this->tokenGenerator = $this->prophesize(CsrfTokenGenerator::class); $this->tokenGenerator->validate(Argument::cetera())->will(function ($args) { return $args[0] === 'token-' . $args[1]; }); $this->machineNameController = new MachineNameController(new PhpTransliteration(), $this->tokenGenerator->reveal()); }
/** * Downloads a tarball of the site configuration. * * @param string $uri * The URI to download. * @param \Symfony\Component\HttpFoundation\Request $request * The request. * * @return \Symfony\Component\HttpFoundation\BinaryFileResponse * The downloaded file. */ public function downloadExport($uri, Request $request) { if ($uri) { // @todo Simplify once https://www.drupal.org/node/2630920 is solved. if (!$this->csrfToken->validate($request->query->get('token'), $uri)) { throw new AccessDeniedHttpException(); } $request = new Request(array('file' => $uri)); return $this->fileDownloadController->download($request, 'temporary'); } }
/** * {@inheritdoc} */ public function determineActiveTheme(RouteMatchInterface $route_match) { $ajax_page_state = $this->requestStack->getCurrentRequest()->request->get('ajax_page_state'); $theme = $ajax_page_state['theme']; $token = $ajax_page_state['theme_token']; // Prevent a request forgery from giving a person access to a theme they // shouldn't be otherwise allowed to see. However, since everyone is // allowed to see the default theme, token validation isn't required for // that, and bypassing it allows most use-cases to work even when accessed // from the page cache. if ($theme === $this->configFactory->get('system.theme')->get('default') || $this->csrfGenerator->validate($token, $theme)) { return $theme; } }
/** * {@inheritdoc} */ public function getCache($form_build_id, &$form_state) { if ($form = $this->keyValueExpirableFactory->get('form')->get($form_build_id)) { $user = $this->currentUser(); if (isset($form['#cache_token']) && $this->csrfToken->validate($form['#cache_token']) || !isset($form['#cache_token']) && $user->isAnonymous()) { if ($stored_form_state = $this->keyValueExpirableFactory->get('form_state')->get($form_build_id)) { // Re-populate $form_state for subsequent rebuilds. $form_state = $stored_form_state + $form_state; // If the original form is contained in include files, load the files. // @see form_load_include() $form_state['build_info'] += array('files' => array()); foreach ($form_state['build_info']['files'] as $file) { if (is_array($file)) { $file += array('type' => 'inc', 'name' => $file['module']); $this->moduleHandler->loadInclude($file['module'], $file['type'], $file['name']); } elseif (file_exists($file)) { require_once DRUPAL_ROOT . '/' . $file; } } // Retrieve the list of previously known safe strings and store it // for this request. // @todo Ensure we are not storing an excessively large string list // in: https://www.drupal.org/node/2295823 $form_state['build_info'] += array('safe_strings' => array()); SafeMarkup::setMultiple($form_state['build_info']['safe_strings']); unset($form_state['build_info']['safe_strings']); } return $form; } } }
/** * Checks access based on a CSRF token for the request. * * @param \Symfony\Component\Routing\Route $route * The route to check against. * @param \Symfony\Component\HttpFoundation\Request $request * The request object. * @param \Drupal\Core\Routing\RouteMatchInterface $route_match * The route match object. * * @return \Drupal\Core\Access\AccessResultInterface * The access result. */ public function access(Route $route, Request $request, RouteMatchInterface $route_match) { $parameters = $route_match->getRawParameters(); $path = ltrim($route->getPath(), '/'); // Replace the path parameters with values from the parameters array. foreach ($parameters as $param => $value) { $path = str_replace("{{$param}}", $value, $path); } if ($this->csrfToken->validate($request->query->get('token'), $path)) { $result = AccessResult::allowed(); } else { $result = AccessResult::forbidden(); } // Not cacheable because the CSRF token is highly dynamic. return $result->setCacheable(FALSE); }
/** * {@inheritdoc} */ public function validateForm($form_id, &$form, FormStateInterface &$form_state) { // If this form is flagged to always validate, ensure that previous runs of // validation are ignored. if ($form_state->isValidationEnforced()) { $form_state->setValidationComplete(FALSE); } // If this form has completed validation, do not validate again. if ($form_state->isValidationComplete()) { return; } // If the session token was set by self::prepareForm(), ensure that it // matches the current user's session. This is duplicate to code in // FormBuilder::doBuildForm() but left to protect any custom form handling // code. if (isset($form['#token'])) { if (!$this->csrfToken->validate($form_state->getValue('form_token'), $form['#token']) || $form_state->hasInvalidToken()) { $this->setInvalidTokenError($form_state); // Stop here and don't run any further validation handlers, because they // could invoke non-safe operations which opens the door for CSRF // vulnerabilities. $this->finalizeValidation($form, $form_state, $form_id); return; } } // Recursively validate each form element. $this->doValidateForm($form, $form_state, $form_id); $this->finalizeValidation($form, $form_state, $form_id); $this->handleErrorsWithLimitedValidation($form, $form_state, $form_id); }
/** * Checks access based on a CSRF token for the request. * * @param \Symfony\Component\Routing\Route $route * The route to check against. * @param \Symfony\Component\HttpFoundation\Request $request * The request object. * * @return string * A \Drupal\Core\Access\AccessInterface constant value. */ public function access(Route $route, Request $request) { // If this is the controller request, check CSRF access as normal. if ($request->attributes->get('_controller_request')) { // @todo Remove dependency on the internal _system_path attribute: // https://www.drupal.org/node/2293501. return $this->csrfToken->validate($request->query->get('token'), $request->attributes->get('_system_path')) ? static::ALLOW : static::KILL; } // Otherwise, this could be another requested access check that we don't // want to check CSRF tokens on. $conjunction = $route->getOption('_access_mode') ?: 'ANY'; // Return ALLOW if all access checks are needed. if ($conjunction == 'ALL') { return static::ALLOW; } else { return static::DENY; } }
/** * @covers ::determineActiveTheme */ public function testDetermineActiveThemeDefaultTheme() { $theme = 'bartik'; // When the theme is the system default, an empty string is provided as the // theme token. See system_js_settings_alter(). $theme_token = ''; $request = new Request([], ['ajax_page_state' => ['theme' => $theme, 'theme_token' => $theme_token]]); $this->requestStack->push($request); $route_match = RouteMatch::createFromRequest($request); $this->tokenGenerator->validate(Argument::cetera())->shouldNotBeCalled(); $result = $this->negotiator->determineActiveTheme($route_match); $this->assertSame($theme, $result); }
/** * Checks access. * * @param \Symfony\Component\HttpFoundation\Request $request * The request object. * @param \Drupal\Core\Session\AccountInterface $account * The currently logged in account. * * @return \Drupal\Core\Access\AccessResultInterface * The access result. */ public function access(Request $request, AccountInterface $account) { $method = $request->getMethod(); // This check only applies if // 1. this is a write operation // 2. the user was successfully authenticated and // 3. the request comes with a session cookie. if (!in_array($method, array('GET', 'HEAD', 'OPTIONS', 'TRACE')) && $account->isAuthenticated() && $this->sessionConfiguration->hasSession($request)) { $csrf_token = $request->headers->get('X-CSRF-Token'); // @todo Remove validate call using 'rest' in 8.3. // Kept here for sessions active during update. if (!$this->csrfToken->validate($csrf_token, self::TOKEN_KEY) && !$this->csrfToken->validate($csrf_token, 'rest')) { return AccessResult::forbidden()->setReason('X-CSRF-Token request header is missing')->setCacheMaxAge(0); } } // Let other access checkers decide if the request is legit. return AccessResult::allowed()->setCacheMaxAge(0); }
/** * {@inheritdoc} */ public function getCache($form_build_id, FormStateInterface $form_state) { if ($form = $this->keyValueExpirableFactory->get('form')->get($form_build_id)) { if (isset($form['#cache_token']) && $this->csrfToken->validate($form['#cache_token']) || !isset($form['#cache_token']) && $this->currentUser->isAnonymous()) { $this->loadCachedFormState($form_build_id, $form_state); // Generate a new #build_id if the cached form was rendered on a // cacheable page. $build_info = $form_state->getBuildInfo(); if (!empty($build_info['immutable'])) { $form['#build_id_old'] = $form['#build_id']; $form['#build_id'] = 'form-' . Crypt::randomBytesBase64(); $form['form_build_id']['#value'] = $form['#build_id']; $form['form_build_id']['#id'] = $form['#build_id']; unset($build_info['immutable']); $form_state->setBuildInfo($build_info); } return $form; } } }
/** * Tests CsrfTokenGenerator::validate() with invalid parameter types. * * @param mixed $token * The token to be validated. * @param mixed $value * (optional) An additional value to base the token on. * * @covers ::validate * @dataProvider providerTestInvalidParameterTypes * @expectedException InvalidArgumentException */ public function testInvalidParameterTypes($token, $value = '') { $this->setupDefaultExpectations(); $this->generator->validate($token, $value); }
/** * {@inheritdoc} */ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state) { // Initialize as unprocessed. $element['#processed'] = FALSE; // Use element defaults. if (isset($element['#type']) && empty($element['#defaults_loaded']) && ($info = $this->elementInfo->getInfo($element['#type']))) { // Overlay $info onto $element, retaining preexisting keys in $element. $element += $info; $element['#defaults_loaded'] = TRUE; } // Assign basic defaults common for all form elements. $element += array('#required' => FALSE, '#attributes' => array(), '#title_display' => 'before', '#description_display' => 'after', '#errors' => NULL); // Special handling if we're on the top level form element. if (isset($element['#type']) && $element['#type'] == 'form') { if (!empty($element['#https']) && !UrlHelper::isExternal($element['#action'])) { global $base_root; // Not an external URL so ensure that it is secure. $element['#action'] = str_replace('http://', 'https://', $base_root) . $element['#action']; } // Store a reference to the complete form in $form_state prior to building // the form. This allows advanced #process and #after_build callbacks to // perform changes elsewhere in the form. $form_state->setCompleteForm($element); // Set a flag if we have a correct form submission. This is always TRUE // for programmed forms coming from self::submitForm(), or if the form_id // coming from the POST data is set and matches the current form_id. $input = $form_state->getUserInput(); if ($form_state->isProgrammed() || !empty($input) && (isset($input['form_id']) && $input['form_id'] == $form_id)) { $form_state->setProcessInput(); if (isset($element['#token'])) { $input = $form_state->getUserInput(); if (empty($input['form_token']) || !$this->csrfToken->validate($input['form_token'], $element['#token'])) { // Set an early form error to block certain input processing since // that opens the door for CSRF vulnerabilities. $this->setInvalidTokenError($form_state); // This value is checked in self::handleInputElement(). $form_state->setInvalidToken(TRUE); // Make sure file uploads do not get processed. $this->requestStack->getCurrentRequest()->files = new FileBag(); } } } else { $form_state->setProcessInput(FALSE); } // All form elements should have an #array_parents property. $element['#array_parents'] = array(); } if (!isset($element['#id'])) { $unprocessed_id = 'edit-' . implode('-', $element['#parents']); $element['#id'] = Html::getUniqueId($unprocessed_id); // Provide a selector usable by JavaScript. As the ID is unique, its not // possible to rely on it in JavaScript. $element['#attributes']['data-drupal-selector'] = Html::getId($unprocessed_id); } else { // Provide a selector usable by JavaScript. As the ID is unique, its not // possible to rely on it in JavaScript. $element['#attributes']['data-drupal-selector'] = Html::getId($element['#id']); } // Add the aria-describedby attribute to associate the form control with its // description. if (!empty($element['#description'])) { $element['#attributes']['aria-describedby'] = $element['#id'] . '--description'; } // Handle input elements. if (!empty($element['#input'])) { $this->handleInputElement($form_id, $element, $form_state); } // Allow for elements to expand to multiple elements, e.g., radios, // checkboxes and files. if (isset($element['#process']) && !$element['#processed']) { foreach ($element['#process'] as $callback) { $complete_form =& $form_state->getCompleteForm(); $element = call_user_func_array($form_state->prepareCallback($callback), array(&$element, &$form_state, &$complete_form)); } $element['#processed'] = TRUE; } // We start off assuming all form elements are in the correct order. $element['#sorted'] = TRUE; // Recurse through all child elements. $count = 0; if (isset($element['#access'])) { $access = $element['#access']; $inherited_access = NULL; if ($access instanceof AccessResultInterface && !$access->isAllowed() || $access === FALSE) { $inherited_access = $access; } } foreach (Element::children($element) as $key) { // Prior to checking properties of child elements, their default // properties need to be loaded. if (isset($element[$key]['#type']) && empty($element[$key]['#defaults_loaded']) && ($info = $this->elementInfo->getInfo($element[$key]['#type']))) { $element[$key] += $info; $element[$key]['#defaults_loaded'] = TRUE; } // Don't squash an existing tree value. if (!isset($element[$key]['#tree'])) { $element[$key]['#tree'] = $element['#tree']; } // Children inherit #access from parent. if (isset($inherited_access)) { $element[$key]['#access'] = $inherited_access; } // Make child elements inherit their parent's #disabled and #allow_focus // values unless they specify their own. foreach (array('#disabled', '#allow_focus') as $property) { if (isset($element[$property]) && !isset($element[$key][$property])) { $element[$key][$property] = $element[$property]; } } // Don't squash existing parents value. if (!isset($element[$key]['#parents'])) { // Check to see if a tree of child elements is present. If so, // continue down the tree if required. $element[$key]['#parents'] = $element[$key]['#tree'] && $element['#tree'] ? array_merge($element['#parents'], array($key)) : array($key); } // Ensure #array_parents follows the actual form structure. $array_parents = $element['#array_parents']; $array_parents[] = $key; $element[$key]['#array_parents'] = $array_parents; // Assign a decimal placeholder weight to preserve original array order. if (!isset($element[$key]['#weight'])) { $element[$key]['#weight'] = $count / 1000; } else { // If one of the child elements has a weight then we will need to sort // later. unset($element['#sorted']); } $element[$key] = $this->doBuildForm($form_id, $element[$key], $form_state); $count++; } // The #after_build flag allows any piece of a form to be altered // after normal input parsing has been completed. if (isset($element['#after_build']) && !isset($element['#after_build_done'])) { foreach ($element['#after_build'] as $callback) { $element = call_user_func_array($form_state->prepareCallback($callback), array($element, &$form_state)); } $element['#after_build_done'] = TRUE; } // If there is a file element, we need to flip a flag so later the // form encoding can be set. if (isset($element['#type']) && $element['#type'] == 'file') { $form_state->setHasFileElement(); } // Final tasks for the form element after self::doBuildForm() has run for // all other elements. if (isset($element['#type']) && $element['#type'] == 'form') { // If there is a file element, we set the form encoding. if ($form_state->hasFileElement()) { $element['#attributes']['enctype'] = 'multipart/form-data'; } // Allow Ajax submissions to the form action to bypass verification. This // is especially useful for multipart forms, which cannot be verified via // a response header. $element['#attached']['drupalSettings']['ajaxTrustedUrl'][$element['#action']] = TRUE; // If a form contains a single textfield, and the ENTER key is pressed // within it, Internet Explorer submits the form with no POST data // identifying any submit button. Other browsers submit POST data as // though the user clicked the first button. Therefore, to be as // consistent as we can be across browsers, if no 'triggering_element' has // been identified yet, default it to the first button. $buttons = $form_state->getButtons(); if (!$form_state->isProgrammed() && !$form_state->getTriggeringElement() && !empty($buttons)) { $form_state->setTriggeringElement($buttons[0]); } $triggering_element = $form_state->getTriggeringElement(); // If the triggering element specifies "button-level" validation and // submit handlers to run instead of the default form-level ones, then add // those to the form state. if (isset($triggering_element['#validate'])) { $form_state->setValidateHandlers($triggering_element['#validate']); } if (isset($triggering_element['#submit'])) { $form_state->setSubmitHandlers($triggering_element['#submit']); } // If the triggering element executes submit handlers, then set the form // state key that's needed for those handlers to run. if (!empty($triggering_element['#executes_submit_callback'])) { $form_state->setSubmitted(); } // Special processing if the triggering element is a button. if (!empty($triggering_element['#is_button'])) { // Because there are several ways in which the triggering element could // have been determined (including from input variables set by // JavaScript or fallback behavior implemented for IE), and because // buttons often have their #name property not derived from their // #parents property, we can't assume that input processing that's // happened up until here has resulted in // $form_state->getValue(BUTTON_NAME) being set. But it's common for // forms to have several buttons named 'op' and switch on // $form_state->getValue('op') during submit handler execution. $form_state->setValue($triggering_element['#name'], $triggering_element['#value']); } } return $element; }
/** * Tests CsrfTokenGenerator::validate() with invalid parameter types. * * @param mixed $token * The token to be validated. * @param mixed $value * (optional) An additional value to base the token on. * * @dataProvider providerTestInvalidParameterTypes * @expectedException InvalidArgumentException */ public function testInvalidParameterTypes($token, $value = '') { // Ensure that there is a valid token seed on the session. $this->generator->get(); $this->generator->validate($token, $value); }