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 }