Commit message: None
CVE: CVE-2016-5225
CWE: CWE-19
Vulnerability description: Blink in Google Chrome prior to 55.0.2883.75 for Mac, Windows and Linux, and 55.0.2883.84 for Android incorrectly handled form actions, which allowed a remote attacker to bypass Content Security Policy via a crafted HTML page.

Project: Chrome

DEFINE_TRACE(HTMLFormElement) {
  visitor->trace(m_pastNamesMap);
  visitor->trace(m_radioButtonGroupScope);
  visitor->trace(m_associatedElements);
  visitor->trace(m_imageElements);
  visitor->trace(m_plannedNavigation);
  HTMLElement::trace(visitor);
}

Project: Chrome

HTMLFormElement::HTMLFormElement(Document& document)
    : HTMLElement(formTag, document),
      m_associatedElementsAreDirty(false),
      m_imageElementsAreDirty(false),
      m_hasElementsAssociatedByParser(false),
      m_hasElementsAssociatedByFormAttribute(false),
      m_didFinishParsingChildren(false),
      m_isInResetFunction(false),
      m_wasDemoted(false) {}

Project: Chrome

const AtomicString& HTMLFormElement::action() const {
  return getAttribute(actionAttr);
}

Project: Chrome

void HTMLFormElement::addToPastNamesMap(Element* element,
                                        const AtomicString& pastName) {
  if (pastName.isEmpty())
    return;
  if (!m_pastNamesMap)
    m_pastNamesMap = new PastNamesMap;
  m_pastNamesMap->set(pastName, element);
}

Project: Chrome

void HTMLFormElement::anonymousNamedGetter(
    const AtomicString& name,
    RadioNodeListOrElement& returnValue) {
  {
    HeapVector<Member<Element>> elements;
    getNamedElements(name, elements);
    if (elements.isEmpty())
      return;
  }

  HeapVector<Member<Element>> elements;
  getNamedElements(name, elements);
  DCHECK(!elements.isEmpty());

  bool onlyMatchImg =
      !elements.isEmpty() && isHTMLImageElement(*elements.first());
  if (onlyMatchImg) {
    UseCounter::count(document(), UseCounter::FormNameAccessForImageElement);
    for (auto& element : elements) {
      if (isHTMLImageElement(*element) && !element->isDescendantOf(this)) {
        UseCounter::count(
            document(), UseCounter::FormNameAccessForNonDescendantImageElement);
        break;
      }
    }
  }
  if (elements.size() == 1) {
    returnValue.setElement(elements.at(0));
    return;
  }

  returnValue.setRadioNodeList(radioNodeList(name, onlyMatchImg));
}

Project: Chrome

void HTMLFormElement::associate(FormAssociatedElement& e) {
  m_associatedElementsAreDirty = true;
  m_associatedElements.clear();
  if (toHTMLElement(e).fastHasAttribute(formAttr))
    m_hasElementsAssociatedByFormAttribute = true;
}

Project: Chrome

void HTMLFormElement::associate(HTMLImageElement& e) {
  m_imageElementsAreDirty = true;
  m_imageElements.clear();
}

Project: Chrome

const FormAssociatedElement::List& HTMLFormElement::associatedElements() const {
  if (!m_associatedElementsAreDirty)
    return m_associatedElements;
  HTMLFormElement* mutableThis = const_cast<HTMLFormElement*>(this);
  Node* scope = mutableThis;
  if (m_hasElementsAssociatedByParser)
    scope = &NodeTraversal::highestAncestorOrSelf(*mutableThis);
  if (isConnected() && m_hasElementsAssociatedByFormAttribute)
    scope = &treeScope().rootNode();
  DCHECK(scope);
  collectAssociatedElements(*scope, mutableThis->m_associatedElements);
  mutableThis->m_associatedElementsAreDirty = false;
  return m_associatedElements;
}

Project: Chrome

bool HTMLFormElement::checkInvalidControlsAndCollectUnhandled(
    HeapVector<Member<HTMLFormControlElement>>* unhandledInvalidControls,
    CheckValidityEventBehavior eventBehavior) {
  const FormAssociatedElement::List& associatedElements =
      this->associatedElements();
  HeapVector<Member<FormAssociatedElement>> elements;
  elements.reserveCapacity(associatedElements.size());
  for (unsigned i = 0; i < associatedElements.size(); ++i)
    elements.append(associatedElements[i]);
  int invalidControlsCount = 0;
  for (unsigned i = 0; i < elements.size(); ++i) {
    if (elements[i]->form() == this && elements[i]->isFormControlElement()) {
      HTMLFormControlElement* control =
          toHTMLFormControlElement(elements[i].get());
      if (control->isSubmittableElement() &&
          !control->checkValidity(unhandledInvalidControls, eventBehavior) &&
          control->formOwner() == this) {
        ++invalidControlsCount;
        if (!unhandledInvalidControls &&
            eventBehavior == CheckValidityDispatchNoEvent)
          return true;
      }
    }
  }
  return invalidControlsCount;
}

Project: Chrome

bool HTMLFormElement::checkValidity() {
  return !checkInvalidControlsAndCollectUnhandled(
      0, CheckValidityDispatchInvalidEvent);
}

Project: Chrome

void HTMLFormElement::collectAssociatedElements(
    Node& root,
    FormAssociatedElement::List& elements) const {
  elements.clear();
  for (HTMLElement& element : Traversal<HTMLElement>::startsAfter(root)) {
    FormAssociatedElement* associatedElement = 0;
    if (element.isFormControlElement())
      associatedElement = toHTMLFormControlElement(&element);
    else if (isHTMLObjectElement(element))
      associatedElement = toHTMLObjectElement(&element);
    else
      continue;
    if (associatedElement->form() == this)
      elements.append(associatedElement);
  }
}

Project: Chrome

void HTMLFormElement::collectImageElements(
    Node& root,
    HeapVector<Member<HTMLImageElement>>& elements) {
  elements.clear();
  for (HTMLImageElement& image :
       Traversal<HTMLImageElement>::startsAfter(root)) {
    if (image.formOwner() == this)
      elements.append(&image);
  }
}

Project: Chrome

HTMLFormElement* HTMLFormElement::create(Document& document) {
  UseCounter::count(document, UseCounter::FormElement);
  return new HTMLFormElement(document);
}

Project: Chrome

void HTMLFormElement::didAssociateByParser() {
  if (!m_didFinishParsingChildren)
    return;
  m_hasElementsAssociatedByParser = true;
  UseCounter::count(document(), UseCounter::FormAssociationByParser);
}

Project: Chrome

void HTMLFormElement::disassociate(FormAssociatedElement& e) {
  m_associatedElementsAreDirty = true;
  m_associatedElements.clear();
  removeFromPastNamesMap(toHTMLElement(e));
}

Project: Chrome

void HTMLFormElement::disassociate(HTMLImageElement& e) {
  m_imageElementsAreDirty = true;
  m_imageElements.clear();
  removeFromPastNamesMap(e);
}

Project: Chrome

Element* HTMLFormElement::elementFromPastNamesMap(
    const AtomicString& pastName) {
  if (pastName.isEmpty() || !m_pastNamesMap)
    return 0;
  Element* element = m_pastNamesMap->get(pastName);
#if DCHECK_IS_ON()
  if (!element)
    return 0;
  SECURITY_DCHECK(toHTMLElement(element)->formOwner() == this);
  if (isHTMLImageElement(*element)) {
    SECURITY_DCHECK(imageElements().find(element) != kNotFound);
  } else if (isHTMLObjectElement(*element)) {
    SECURITY_DCHECK(associatedElements().find(toHTMLObjectElement(element)) !=
                    kNotFound);
  } else {
    SECURITY_DCHECK(associatedElements().find(
                        toHTMLFormControlElement(element)) != kNotFound);
  }
#endif
  return element;
}

Project: Chrome

HTMLFormControlsCollection* HTMLFormElement::elements() {
  return ensureCachedCollection<HTMLFormControlsCollection>(FormControls);
}

Project: Chrome

HTMLFormControlElement* HTMLFormElement::findDefaultButton() const {
  for (const auto& element : associatedElements()) {
    if (!element->isFormControlElement())
      continue;
    HTMLFormControlElement* control = toHTMLFormControlElement(element);
    if (control->canBeSuccessfulSubmitButton())
      return control;
  }
  return nullptr;
}

Project: Chrome

void HTMLFormElement::finishParsingChildren() {
  HTMLElement::finishParsingChildren();
  document().formController().restoreControlStateIn(*this);
  m_didFinishParsingChildren = true;
}

Project: Chrome

void HTMLFormElement::getNamedElements(
    const AtomicString& name,
    HeapVector<Member<Element>>& namedItems) {
  elements()->namedItems(name, namedItems);

  Element* elementFromPast = elementFromPastNamesMap(name);
  if (namedItems.size() && namedItems.first() != elementFromPast) {
    addToPastNamesMap(namedItems.first().get(), name);
  } else if (elementFromPast && namedItems.isEmpty()) {
    namedItems.append(elementFromPast);
    UseCounter::count(document(), UseCounter::FormNameAccessForPastNamesMap);
  }
}

Project: Chrome

void HTMLFormElement::handleLocalEvents(Event& event) {
  Node* targetNode = event.target()->toNode();
  if (event.eventPhase() != Event::kCapturingPhase && targetNode &&
      targetNode != this && (event.type() == EventTypeNames::submit ||
                             event.type() == EventTypeNames::reset)) {
    event.stopPropagation();
    return;
  }
  HTMLElement::handleLocalEvents(event);
}

Project: Chrome

bool HTMLFormElement::hasLegalLinkAttribute(const QualifiedName& name) const {
  return name == actionAttr || HTMLElement::hasLegalLinkAttribute(name);
}

Project: Chrome

const HeapVector<Member<HTMLImageElement>>& HTMLFormElement::imageElements() {
  if (!m_imageElementsAreDirty)
    return m_imageElements;
  collectImageElements(m_hasElementsAssociatedByParser
                           ? NodeTraversal::highestAncestorOrSelf(*this)
                           : *this,
                       m_imageElements);
  m_imageElementsAreDirty = false;
  return m_imageElements;
}

Project: Chrome

Node::InsertionNotificationRequest HTMLFormElement::insertedInto(
    ContainerNode* insertionPoint) {
  HTMLElement::insertedInto(insertionPoint);
  logAddElementIfIsolatedWorldAndInDocument("form", methodAttr, actionAttr);
  if (insertionPoint->isConnected())
    this->document().didAssociateFormControl(this);
  return InsertionDone;
}

Project: Chrome

void HTMLFormElement::invalidateDefaultButtonStyle() const {
  for (const auto& control : associatedElements()) {
    if (!control->isFormControlElement())
      continue;
    if (toHTMLFormControlElement(control)->canBeSuccessfulSubmitButton())
      toHTMLFormControlElement(control)->pseudoStateChanged(
          CSSSelector::PseudoDefault);
  }
}

Project: Chrome

bool HTMLFormElement::isValidElement() {
  return !checkInvalidControlsAndCollectUnhandled(0,
                                                  CheckValidityDispatchNoEvent);
}

Project: Chrome

bool HTMLFormElement::layoutObjectIsNeeded(const ComputedStyle& style) {
  if (!m_wasDemoted)
    return HTMLElement::layoutObjectIsNeeded(style);

  ContainerNode* node = parentNode();
  if (!node || !node->layoutObject())
    return HTMLElement::layoutObjectIsNeeded(style);
  LayoutObject* parentLayoutObject = node->layoutObject();
  bool parentIsTableElementPart =
      (parentLayoutObject->isTable() && isHTMLTableElement(*node)) ||
      (parentLayoutObject->isTableRow() && isHTMLTableRowElement(*node)) ||
      (parentLayoutObject->isTableSection() && node->hasTagName(tbodyTag)) ||
      (parentLayoutObject->isLayoutTableCol() && node->hasTagName(colTag)) ||
      (parentLayoutObject->isTableCell() && isHTMLTableRowElement(*node));

  if (!parentIsTableElementPart)
    return true;

  EDisplay display = style.display();
  bool formIsTablePart =
      display == EDisplay::Table || display == EDisplay::InlineTable ||
      display == EDisplay::TableRowGroup ||
      display == EDisplay::TableHeaderGroup ||
      display == EDisplay::TableFooterGroup || display == EDisplay::TableRow ||
      display == EDisplay::TableColumnGroup ||
      display == EDisplay::TableColumn || display == EDisplay::TableCell ||
      display == EDisplay::TableCaption;

  return formIsTablePart;
}

Project: Chrome

unsigned HTMLFormElement::length() const {
  const FormAssociatedElement::List& elements = associatedElements();
  unsigned len = 0;
  for (unsigned i = 0; i < elements.size(); ++i) {
    if (elements[i]->isEnumeratable())
      ++len;
  }
  return len;
}

Project: Chrome

bool HTMLFormElement::matchesValidityPseudoClasses() const {
  return true;
}

Project: Chrome

void notifyFormRemovedFromTree(const T& elements, Node& root) {
  for (const auto& element : elements)
    element->formRemovedFromTree(root);
}

Project: Chrome

void HTMLFormElement::parseAttribute(const QualifiedName& name,
                                     const AtomicString& oldValue,
                                     const AtomicString& value) {
  if (name == actionAttr) {
    m_attributes.parseAction(value);
    logUpdateAttributeIfIsolatedWorldAndInDocument("form", actionAttr, oldValue,
                                                   value);

    if (document().getInsecureRequestPolicy() & kUpgradeInsecureRequests)
      return;
    KURL actionURL = document().completeURL(m_attributes.action().isEmpty()
                                                ? document().url().getString()
                                                : m_attributes.action());
    if (MixedContentChecker::isMixedFormAction(document().frame(), actionURL))
      UseCounter::count(document().frame(),
                        UseCounter::MixedContentFormPresent);
  } else if (name == targetAttr) {
    m_attributes.setTarget(value);
  } else if (name == methodAttr) {
    m_attributes.updateMethodType(value);
  } else if (name == enctypeAttr) {
    m_attributes.updateEncodingType(value);
  } else if (name == accept_charsetAttr) {
    m_attributes.setAcceptCharset(value);
  } else {
    HTMLElement::parseAttribute(name, oldValue, value);
  }
}

Project: Chrome

void HTMLFormElement::prepareForSubmission(
    Event* event,
    HTMLFormControlElement* submitButton) {
  LocalFrame* frame = document().frame();
  if (!frame || m_isSubmitting || m_inUserJSSubmitEvent)
    return;

  if (document().isSandboxed(SandboxForms)) {
    document().addConsoleMessage(ConsoleMessage::create(
        SecurityMessageSource, ErrorMessageLevel,
        "Blocked form submission to '" + m_attributes.action() +
            "' because the form's frame is sandboxed and the 'allow-forms' "
            "permission is not set."));
    return;
  }

  bool skipValidation = !document().page() || noValidate();
  DCHECK(event);
  if (submitButton && submitButton->formNoValidate())
    skipValidation = true;

  UseCounter::count(document(), UseCounter::FormSubmissionStarted);
  if (!skipValidation && !validateInteractively())
    return;

  bool shouldSubmit;
  {
    AutoReset<bool> submitEventHandlerScope(&m_inUserJSSubmitEvent, true);
    frame->loader().client()->dispatchWillSendSubmitEvent(this);
    shouldSubmit =
        dispatchEvent(Event::createCancelableBubble(EventTypeNames::submit)) ==
        DispatchEventResult::NotCanceled;
  }
  if (shouldSubmit) {
    m_plannedNavigation = nullptr;
    submit(event, submitButton);
  }
  if (!m_plannedNavigation)
    return;
  AutoReset<bool> submitScope(&m_isSubmitting, true);
  scheduleFormSubmission(m_plannedNavigation);
  m_plannedNavigation = nullptr;
}

Project: Chrome

void HTMLFormElement::removeFromPastNamesMap(HTMLElement& element) {
  if (!m_pastNamesMap)
    return;
  for (auto& it : *m_pastNamesMap) {
    if (it.value == &element) {
      it.value = nullptr;
    }
  }
}

Project: Chrome

void HTMLFormElement::removedFrom(ContainerNode* insertionPoint) {
  if (m_hasElementsAssociatedByParser) {
    Node& root = NodeTraversal::highestAncestorOrSelf(*this);
    if (!m_associatedElementsAreDirty) {
      FormAssociatedElement::List elements(associatedElements());
      notifyFormRemovedFromTree(elements, root);
    } else {
      FormAssociatedElement::List elements;
      collectAssociatedElements(
          NodeTraversal::highestAncestorOrSelf(*insertionPoint), elements);
      notifyFormRemovedFromTree(elements, root);
      collectAssociatedElements(root, elements);
      notifyFormRemovedFromTree(elements, root);
    }

    if (!m_imageElementsAreDirty) {
      HeapVector<Member<HTMLImageElement>> images(imageElements());
      notifyFormRemovedFromTree(images, root);
    } else {
      HeapVector<Member<HTMLImageElement>> images;
      collectImageElements(
          NodeTraversal::highestAncestorOrSelf(*insertionPoint), images);
      notifyFormRemovedFromTree(images, root);
      collectImageElements(root, images);
      notifyFormRemovedFromTree(images, root);
    }
  }
  document().formController().willDeleteForm(this);
  HTMLElement::removedFrom(insertionPoint);
}

Project: Chrome

bool HTMLFormElement::reportValidity() {
  return validateInteractively();
}

Project: Chrome

void HTMLFormElement::reset() {
  LocalFrame* frame = document().frame();
  if (m_isInResetFunction || !frame)
    return;

  m_isInResetFunction = true;

  if (dispatchEvent(Event::createCancelableBubble(EventTypeNames::reset)) !=
      DispatchEventResult::NotCanceled) {
    m_isInResetFunction = false;
    return;
  }

  const FormAssociatedElement::List& elements = associatedElements();
  for (unsigned i = 0; i < elements.size(); ++i) {
    if (elements[i]->isFormControlElement())
      toHTMLFormControlElement(elements[i])->reset();
  }

  m_isInResetFunction = false;
}

Project: Chrome

void HTMLFormElement::setDemoted(bool demoted) {
  if (demoted)
    UseCounter::count(document(), UseCounter::DemotedFormElement);
  m_wasDemoted = demoted;
}

Project: Chrome

void HTMLFormElement::setMethod(const AtomicString& value) {
  setAttribute(methodAttr, value);
}

Project: Chrome

void HTMLFormElement::submit(Event* event,
                             HTMLFormControlElement* submitButton) {
  FrameView* view = document().view();
  LocalFrame* frame = document().frame();
  if (!view || !frame || !frame->page())
    return;

  if (!isConnected()) {
    document().addConsoleMessage(ConsoleMessage::create(
        JSMessageSource, WarningMessageLevel,
        "Form submission canceled because the form is not connected"));
    return;
  }

  if (m_isSubmitting)
    return;

  EventQueueScope scopeForDialogClose;
  AutoReset<bool> submitScope(&m_isSubmitting, true);

  if (event && !submitButton) {
    for (const auto& associatedElement : associatedElements()) {
      if (!associatedElement->isFormControlElement())
        continue;
      HTMLFormControlElement* control =
          toHTMLFormControlElement(associatedElement);
      DCHECK(!control->isActivatedSubmit());
      if (control->isSuccessfulSubmitButton()) {
        submitButton = control;
        break;
      }
    }
  }

  FormSubmission* formSubmission =
      FormSubmission::create(this, m_attributes, event, submitButton);
  if (formSubmission->method() == FormSubmission::DialogMethod) {
    submitDialog(formSubmission);
  } else if (m_inUserJSSubmitEvent) {
    m_plannedNavigation = formSubmission;
  } else {
    scheduleFormSubmission(formSubmission);
  }
}

Project: Chrome

void HTMLFormElement::submitDialog(FormSubmission* formSubmission) {
  for (Node* node = this; node; node = node->parentOrShadowHostNode()) {
    if (isHTMLDialogElement(*node)) {
      toHTMLDialogElement(*node).closeDialog(formSubmission->result());
      return;
    }
  }
}

Project: Chrome

void HTMLFormElement::submitFromJavaScript() {
  submit(nullptr, nullptr);
}

Project: Chrome

bool HTMLFormElement::validateInteractively() {
  UseCounter::count(document(), UseCounter::FormValidationStarted);
  const FormAssociatedElement::List& elements = associatedElements();
  for (unsigned i = 0; i < elements.size(); ++i) {
    if (elements[i]->isFormControlElement())
      toHTMLFormControlElement(elements[i])->hideVisibleValidationMessage();
  }

  HeapVector<Member<HTMLFormControlElement>> unhandledInvalidControls;
  if (!checkInvalidControlsAndCollectUnhandled(
          &unhandledInvalidControls, CheckValidityDispatchInvalidEvent))
    return true;
  UseCounter::count(document(), UseCounter::FormValidationAbortedSubmission);

  document().updateStyleAndLayoutIgnorePendingStylesheets();

  for (unsigned i = 0; i < unhandledInvalidControls.size(); ++i) {
    HTMLFormControlElement* unhandled = unhandledInvalidControls[i].get();
    if (unhandled->isFocusable()) {
      unhandled->showValidationMessage();
      UseCounter::count(document(), UseCounter::FormValidationShowedMessage);
      break;
    }
  }
  if (document().frame()) {
    for (unsigned i = 0; i < unhandledInvalidControls.size(); ++i) {
      HTMLFormControlElement* unhandled = unhandledInvalidControls[i].get();
      if (unhandled->isFocusable())
        continue;
      String message(
          "An invalid form control with name='%name' is not focusable.");
      message.replace("%name", unhandled->name());
      document().addConsoleMessage(ConsoleMessage::create(
          RenderingMessageSource, ErrorMessageLevel, message));
    }
  }
  return false;
}

Project: Chrome

HTMLFormElement::~HTMLFormElement() {}