tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

JSValidatorChild.cpp (7720B)


      1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 #include "mozilla/dom/JSValidatorChild.h"
      8 
      9 #include "js/CompileOptions.h"
     10 #include "js/JSON.h"
     11 #include "js/SourceText.h"
     12 #include "js/experimental/CompileScript.h"
     13 #include "js/experimental/JSStencil.h"
     14 #include "mozilla/Encoding.h"
     15 #include "mozilla/dom/JSOracleChild.h"
     16 #include "mozilla/dom/ScriptDecoding.h"
     17 #include "mozilla/ipc/Endpoint.h"
     18 #include "xpcpublic.h"
     19 
     20 using namespace mozilla::dom;
     21 using Encoding = mozilla::Encoding;
     22 
     23 mozilla::UniquePtr<mozilla::Decoder> TryGetDecoder(
     24    const mozilla::Span<const uint8_t>& aSourceBytes,
     25    const nsACString& aContentCharset, const nsAString& aHintCharset,
     26    const nsAString& aDocumentCharset) {
     27  const Encoding* encoding;
     28  mozilla::UniquePtr<mozilla::Decoder> unicodeDecoder;
     29 
     30  std::tie(encoding, std::ignore) = Encoding::ForBOM(aSourceBytes);
     31  if (encoding) {
     32    unicodeDecoder = encoding->NewDecoderWithBOMRemoval();
     33  }
     34 
     35  if (!unicodeDecoder) {
     36    encoding = Encoding::ForLabel(aContentCharset);
     37    if (encoding) {
     38      unicodeDecoder = encoding->NewDecoderWithoutBOMHandling();
     39    }
     40 
     41    if (!unicodeDecoder) {
     42      encoding = Encoding::ForLabel(aHintCharset);
     43      if (encoding) {
     44        unicodeDecoder = encoding->NewDecoderWithoutBOMHandling();
     45      }
     46    }
     47 
     48    if (!unicodeDecoder) {
     49      encoding = Encoding::ForLabel(aDocumentCharset);
     50      if (encoding) {
     51        unicodeDecoder = encoding->NewDecoderWithoutBOMHandling();
     52      }
     53    }
     54  }
     55 
     56  if (!unicodeDecoder && !IsUtf8(mozilla::Span(reinterpret_cast<const char*>(
     57                                                   aSourceBytes.Elements()),
     58                                               aSourceBytes.Length()))) {
     59    // Curiously, there are various callers that don't pass aDocument. The
     60    // fallback in the old code was ISO-8859-1, which behaved like
     61    // windows-1252.
     62    unicodeDecoder = WINDOWS_1252_ENCODING->NewDecoderWithoutBOMHandling();
     63  }
     64 
     65  return unicodeDecoder;
     66 }
     67 
     68 mozilla::ipc::IPCResult JSValidatorChild::RecvIsOpaqueResponseAllowed(
     69    IsOpaqueResponseAllowedResolver&& aResolver) {
     70  mResolver.emplace(aResolver);
     71 
     72  return IPC_OK();
     73 }
     74 
     75 mozilla::ipc::IPCResult JSValidatorChild::RecvOnDataAvailable(Shmem&& aData) {
     76  if (!mResolver) {
     77    MOZ_ASSERT(!CanSend());
     78    return IPC_OK();
     79  }
     80 
     81  if (!mSourceBytes.Append(Span(aData.get<char>(), aData.Size<char>()),
     82                           mozilla::fallible)) {
     83    // To prevent an attacker from flood the validation process,
     84    // we don't validate here.
     85    Resolve(ValidatorResult::Failure);
     86  }
     87  DeallocShmem(aData);
     88 
     89  return IPC_OK();
     90 }
     91 
     92 mozilla::ipc::IPCResult JSValidatorChild::RecvOnStopRequest(
     93    const nsresult& aReason, const nsACString& aContentCharset,
     94    const nsAString& aHintCharset, const nsAString& aDocumentCharset) {
     95  if (!mResolver) {
     96    return IPC_OK();
     97  }
     98 
     99  if (NS_FAILED(aReason)) {
    100    Resolve(ValidatorResult::Failure);
    101  } else if (mSourceBytes.IsEmpty()) {
    102    // The empty document parses as JavaScript.
    103    Resolve(ValidatorResult::JavaScript);
    104  } else {
    105    UniquePtr<Decoder> unicodeDecoder = TryGetDecoder(
    106        mSourceBytes, aContentCharset, aHintCharset, aDocumentCharset);
    107 
    108    if (!unicodeDecoder) {
    109      Resolve(ShouldAllowJS(mSourceBytes));
    110    } else {
    111      BufferUniquePtr<Utf8Unit[]> buffer;
    112      auto result = GetUTF8EncodedContent(mSourceBytes, buffer, unicodeDecoder);
    113      if (result.isErr()) {
    114        Resolve(ValidatorResult::Failure);
    115      } else {
    116        Resolve(ShouldAllowJS(result.unwrap()));
    117      }
    118    }
    119  }
    120 
    121  return IPC_OK();
    122 }
    123 
    124 void JSValidatorChild::ActorDestroy(ActorDestroyReason aReason) {
    125  if (mResolver) {
    126    Resolve(ValidatorResult::Failure);
    127  }
    128 };
    129 
    130 void JSValidatorChild::Resolve(ValidatorResult aResult) {
    131  MOZ_ASSERT(mResolver);
    132  Maybe<Shmem> data = Nothing();
    133  if (aResult == ValidatorResult::JavaScript && !mSourceBytes.IsEmpty()) {
    134    Shmem sharedData;
    135    nsresult rv =
    136        JSValidatorUtils::CopyCStringToShmem(this, mSourceBytes, sharedData);
    137    if (NS_SUCCEEDED(rv)) {
    138      data = Some(std::move(sharedData));
    139    }
    140  }
    141 
    142  mResolver.ref()(std::tuple<mozilla::Maybe<Shmem>&&, const ValidatorResult&>(
    143      std::move(data), aResult));
    144  mResolver.reset();
    145 }
    146 
    147 mozilla::Result<mozilla::Span<const char>, nsresult>
    148 JSValidatorChild::GetUTF8EncodedContent(
    149    const mozilla::Span<const uint8_t>& aData,
    150    BufferUniquePtr<Utf8Unit[]>& aBuffer, UniquePtr<Decoder>& aDecoder) {
    151  MOZ_ASSERT(aDecoder);
    152  // We need the output buffer to be UTF8
    153  CheckedInt<size_t> bufferLength =
    154      ScriptDecoding<Utf8Unit>::MaxBufferLength(aDecoder, aData.Length());
    155  if (!bufferLength.isValid()) {
    156    return mozilla::Err(NS_ERROR_FAILURE);
    157  }
    158 
    159  CheckedInt<size_t> bufferByteSize = bufferLength * sizeof(Utf8Unit);
    160  if (!bufferByteSize.isValid()) {
    161    return mozilla::Err(NS_ERROR_FAILURE);
    162  }
    163 
    164  aBuffer.reset(static_cast<Utf8Unit*>(js_malloc(bufferByteSize.value())));
    165  if (!aBuffer) {
    166    return mozilla::Err(NS_ERROR_FAILURE);
    167  }
    168 
    169  size_t written = ScriptDecoding<Utf8Unit>::DecodeInto(
    170      aDecoder, aData, Span(aBuffer.get(), bufferLength.value()),
    171      /* aEndOfSource = */ true);
    172  MOZ_ASSERT(written <= bufferLength.value());
    173  MOZ_ASSERT(
    174      IsUtf8(Span(reinterpret_cast<const char*>(aBuffer.get()), written)));
    175 
    176  return Span(reinterpret_cast<const char*>(aBuffer.get()), written);
    177 }
    178 
    179 JSValidatorChild::ValidatorResult JSValidatorChild::ShouldAllowJS(
    180    const mozilla::Span<const char>& aSpan) const {
    181  // It's possible that the data we get is not valid UTF-8, so aSpan
    182  // ends empty here. We should treat it as a failure because this
    183  // is not valid JS.
    184  if (aSpan.IsEmpty()) {
    185    return ValidatorResult::Failure;
    186  }
    187 
    188  MOZ_DIAGNOSTIC_ASSERT(IsUtf8(aSpan));
    189 
    190  JS::FrontendContext* fc = JSOracleChild::JSFrontendContext();
    191  if (!fc) {
    192    return ValidatorResult::Failure;
    193  }
    194 
    195  JS::SourceText<Utf8Unit> srcBuf;
    196  if (!srcBuf.init(fc, aSpan.Elements(), aSpan.Length(),
    197                   JS::SourceOwnership::Borrowed)) {
    198    JS::ClearFrontendErrors(fc);
    199    return ValidatorResult::Failure;
    200  }
    201 
    202  // Parse to JavaScript
    203  JS::PrefableCompileOptions prefableOptions;
    204  xpc::SetPrefableCompileOptions(prefableOptions);
    205  // For the syntax validation purpose, asm.js doesn't need to be enabled.
    206  prefableOptions.setAsmJSOption(JS::AsmJSOption::DisabledByAsmJSPref);
    207 
    208  JS::CompileOptions options(prefableOptions);
    209  RefPtr<JS::Stencil> stencil =
    210      JS::CompileGlobalScriptToStencil(fc, options, srcBuf);
    211 
    212  if (!stencil) {
    213    JS::ClearFrontendErrors(fc);
    214    return ValidatorResult::Other;
    215  }
    216 
    217  MOZ_ASSERT(!aSpan.IsEmpty());
    218 
    219  // Parse to JSON
    220  if (IsAscii(aSpan)) {
    221    // Ascii is a subset of Latin1, and JS_ParseJSON can take Latin1 directly
    222    if (JS::IsValidJSON(
    223            reinterpret_cast<const JS::Latin1Char*>(aSpan.Elements()),
    224            aSpan.Length())) {
    225      return ValidatorResult::JSON;
    226    }
    227  } else {
    228    nsString decoded;
    229    nsresult rv = UTF_8_ENCODING->DecodeWithBOMRemoval(
    230        Span(reinterpret_cast<const uint8_t*>(aSpan.Elements()),
    231             aSpan.Length()),
    232        decoded);
    233    if (NS_FAILED(rv)) {
    234      return ValidatorResult::Failure;
    235    }
    236 
    237    if (JS::IsValidJSON(decoded.BeginReading(), decoded.Length())) {
    238      return ValidatorResult::JSON;
    239    }
    240  }
    241 
    242  // Since the JSON parsing failed, we confirmed the file is Javascript and not
    243  // JSON.
    244  return ValidatorResult::JavaScript;
    245 }