AboutHttpService - forbid detailed info for non-local origin, introduce isOriginAllowed to ensure that access policy is clear

GitOrigin-RevId: 3e79479484eb5649560aae5f7033e32af413f987
This commit is contained in:
Vladimir Krivosheev
2020-06-25 14:06:37 +03:00
committed by intellij-monorepo-bot
parent 6d4521d74c
commit b573f79a61
8 changed files with 62 additions and 90 deletions
@@ -10,6 +10,7 @@ import com.intellij.openapi.application.ex.ApplicationInfoEx
import com.intellij.openapi.fileTypes.FileTypeRegistry
import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream
import com.intellij.util.PlatformUtils
import com.intellij.util.io.isLocalOrigin
import com.intellij.util.io.jackson.array
import com.intellij.util.io.jackson.obj
import com.intellij.util.io.origin
@@ -47,58 +48,61 @@ import java.io.OutputStream
internal class AboutHttpService : RestService() {
override fun getServiceName() = "about"
override fun isHostTrusted(request: FullHttpRequest, urlDecoder: QueryStringDecoder): Boolean {
return isTrustedOrigin(request) || super.isHostTrusted(request, urlDecoder)
}
override fun isAccessible(request: HttpRequest): Boolean {
return isTrustedOrigin(request) || super.isAccessible(request)
override fun isOriginAllowed(request: HttpRequest): OriginCheckResult {
val originAllowed = super.isOriginAllowed(request)
if (originAllowed == OriginCheckResult.FORBID) {
val origin = request.origin ?: return OriginCheckResult.FORBID
@Suppress("SpellCheckingInspection")
return if (origin.matches(Regex("https://([a-z0-9-]+\\.)*hyperskill.org$"))) OriginCheckResult.ALLOW else OriginCheckResult.FORBID
}
return originAllowed
}
override fun execute(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): String? {
val byteOut = BufferExposingByteArrayOutputStream()
writeApplicationInfoJson(byteOut, urlDecoder)
writeApplicationInfoJson(byteOut, urlDecoder, request.isLocalOrigin())
send(byteOut, request, context)
return null
}
}
private fun isTrustedOrigin(request: HttpRequest): Boolean {
val origin = request.origin ?: return false
@Suppress("SpellCheckingInspection")
return origin.matches("https?://([a-z0-9-]+\\.)*hyperskill.org$".toRegex())
}
fun writeApplicationInfoJson(out: OutputStream, urlDecoder: QueryStringDecoder?, isLocalOrigin: Boolean) {
JsonFactory().createGenerator(out).useDefaultPrettyPrinter().use { writer ->
writer.obj {
writeAboutJson(writer)
fun writeApplicationInfoJson(out: OutputStream, urlDecoder: QueryStringDecoder?) {
val writer = JsonFactory().createGenerator(out).useDefaultPrettyPrinter()
writer.obj {
writeAboutJson(writer)
if (urlDecoder != null && getBooleanParameter("registeredFileTypes", urlDecoder)) {
writer.array("registeredFileTypes") {
for (fileType in FileTypeRegistry.getInstance().registeredFileTypes) {
writer.obj {
writer.writeStringField("name", fileType.name)
writer.writeStringField("description", fileType.description)
writer.writeBooleanField("isBinary", fileType.isBinary)
// registeredFileTypes and more args are supported only for explicitly trusted origins
if (!isLocalOrigin) {
return
}
if (urlDecoder != null && getBooleanParameter("registeredFileTypes", urlDecoder)) {
writer.array("registeredFileTypes") {
for (fileType in FileTypeRegistry.getInstance().registeredFileTypes) {
writer.obj {
writer.writeStringField("name", fileType.name)
writer.writeStringField("description", fileType.description)
writer.writeBooleanField("isBinary", fileType.isBinary)
}
}
}
}
}
if (urlDecoder != null && getBooleanParameter("more", urlDecoder)) {
val appInfo = ApplicationInfoEx.getInstanceEx()
writer.writeStringField("vendor", appInfo.companyName)
writer.writeBooleanField("isEAP", appInfo.isEAP)
writer.writeStringField("productCode", appInfo.build.productCode)
writer.writeNumberField("buildDate", appInfo.buildDate.time.time)
writer.writeBooleanField("isSnapshot", appInfo.build.isSnapshot)
writer.writeStringField("configPath", PathManager.getConfigPath())
writer.writeStringField("systemPath", PathManager.getSystemPath())
writer.writeStringField("binPath", PathManager.getBinPath())
writer.writeStringField("logPath", PathManager.getLogPath())
writer.writeStringField("homePath", PathManager.getHomePath())
if (urlDecoder != null && getBooleanParameter("more", urlDecoder)) {
val appInfo = ApplicationInfoEx.getInstanceEx()
writer.writeStringField("vendor", appInfo.companyName)
writer.writeBooleanField("isEAP", appInfo.isEAP)
writer.writeStringField("productCode", appInfo.build.productCode)
writer.writeNumberField("buildDate", appInfo.buildDate.time.time)
writer.writeBooleanField("isSnapshot", appInfo.build.isSnapshot)
writer.writeStringField("configPath", PathManager.getConfigPath())
writer.writeStringField("systemPath", PathManager.getSystemPath())
writer.writeStringField("binPath", PathManager.getBinPath())
writer.writeStringField("logPath", PathManager.getLogPath())
writer.writeStringField("homePath", PathManager.getHomePath())
}
}
}
writer.close()
}
fun writeAboutJson(writer: JsonGenerator) {
@@ -1,18 +1,4 @@
/*
* Copyright 2000-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.ide;
import com.google.gson.stream.JsonReader;
@@ -169,7 +155,7 @@ final class DiffHttpService extends RestService {
}
@Override
public boolean isAccessible(@NotNull HttpRequest request) {
return true;
protected @NotNull OriginCheckResult isOriginAllowed(@NotNull HttpRequest request) {
return OriginCheckResult.ASK_CONFIRMATION;
}
}
@@ -28,7 +28,7 @@ import java.net.URISyntaxException
internal class InstallPluginService : RestService() {
override fun getServiceName() = "installPlugin"
override fun isAccessible(request: HttpRequest) = true
override fun isOriginAllowed(request: HttpRequest) = OriginCheckResult.ASK_CONFIRMATION
var isAvailable = true
@@ -65,6 +65,8 @@ internal class OpenFileHttpService : RestService() {
override fun isMethodSupported(method: HttpMethod) = method === HttpMethod.GET || method === HttpMethod.POST
override fun isOriginAllowed(request: HttpRequest) = OriginCheckResult.ASK_CONFIRMATION
override fun execute(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): String? {
val keepAlive = HttpUtil.isKeepAlive(request)
val channel = context.channel()
@@ -170,8 +172,6 @@ internal class OpenFileHttpService : RestService() {
session.launch()
return mainTask.promise
}
override fun isAccessible(request: HttpRequest) = true
}
internal class OpenFileRequest {
@@ -59,7 +59,7 @@ final class ProjectSetRequestHandler extends RestService {
}
@Override
public boolean isAccessible(@NotNull HttpRequest request) {
return true;
protected @NotNull OriginCheckResult isOriginAllowed(@NotNull HttpRequest request) {
return OriginCheckResult.ASK_CONFIRMATION;
}
}
@@ -244,7 +244,7 @@ abstract class RestService : HttpRequestHandler() {
@Throws(InterruptedException::class, InvocationTargetException::class)
// e.g. upsource trust to configured host
protected open fun isHostTrusted(request: FullHttpRequest): Boolean {
if (request.isSignedRequest()) {
if (request.isSignedRequest() || isOriginAllowed(request) == OriginCheckResult.ALLOW) {
return true
}
@@ -2,12 +2,7 @@
package org.jetbrains.ide
import com.intellij.diagnostic.StartUpPerformanceService
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ex.ApplicationInfoEx
import com.intellij.openapi.diagnostic.logger
import com.intellij.util.io.getHostName
import com.intellij.util.io.origin
import com.intellij.util.net.NetUtils
import io.netty.buffer.Unpooled
import io.netty.channel.ChannelHandlerContext
import io.netty.handler.codec.http.FullHttpRequest
@@ -15,24 +10,11 @@ import io.netty.handler.codec.http.HttpRequest
import io.netty.handler.codec.http.QueryStringDecoder
import org.jetbrains.io.response
private val LOG = logger<StartUpMeasurementService>()
internal class StartUpMeasurementService : RestService() {
override fun getServiceName() = "startUpMeasurement"
override fun isAccessible(request: HttpRequest): Boolean {
if (super.isAccessible(request)) {
return true
}
// expose externally to use visualizer front-end
// personal data is not exposed (but someone can say that 3rd plugin class names should be not exposed),
// so, limit to dev builds only (EAP builds are not allowed too) or app in an internal mode (and still only for known hosts)
return isTrustedHostName(request) && (ApplicationManager.getApplication().isInternal || ApplicationInfoEx.getInstanceEx().build.isSnapshot)
}
override fun isHostTrusted(request: FullHttpRequest, urlDecoder: QueryStringDecoder): Boolean {
return isTrustedHostName(request) || super.isHostTrusted(request, urlDecoder)
override fun isOriginAllowed(request: HttpRequest): OriginCheckResult {
return if (request.origin == "https://ij-perf.jetbrains.com") OriginCheckResult.ALLOW else super.isOriginAllowed(request)
}
override fun execute(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): String? {
@@ -42,12 +24,4 @@ internal class StartUpMeasurementService : RestService() {
sendResponse(request, context, response)
return null
}
}
private fun isTrustedHostName(request: HttpRequest): Boolean {
val hostName = getHostName(request) ?: return false
if (!NetUtils.isLocalhost(hostName)) {
LOG.error("Expected 'request.hostName' to be localhost. hostName=$hostName, origin=${request.origin}")
}
return hostName == "ij-perf.jetbrains.com" || hostName == "ij-perf.develar.org" || NetUtils.isLocalhost(hostName)
}
@@ -18,6 +18,12 @@ import java.io.IOException
import java.util.*
abstract class HttpRequestHandler {
enum class OriginCheckResult {
ALLOW, FORBID,
// any origin is allowed but user confirmation is required
ASK_CONFIRMATION
}
companion object {
// Your handler will be instantiated on first user request
val EP_NAME = ExtensionPointName<HttpRequestHandler>("com.intellij.httpRequestHandler")
@@ -45,10 +51,12 @@ abstract class HttpRequestHandler {
val hostName = getHostName(request)
// If attacker.com DNS rebound to 127.0.0.1 and user open site directly - no Origin or Referrer headers.
// So we should check Host header.
return hostName != null && isOriginAllowed(request) && isLocalHost(hostName)
return hostName != null && isOriginAllowed(request) != OriginCheckResult.FORBID && isLocalHost(hostName)
}
protected open fun isOriginAllowed(request: HttpRequest) = request.isLocalOrigin()
protected open fun isOriginAllowed(request: HttpRequest): OriginCheckResult {
return if (request.isLocalOrigin()) OriginCheckResult.ALLOW else OriginCheckResult.FORBID
}
open fun isSupported(request: FullHttpRequest): Boolean {
return request.method() === HttpMethod.GET || request.method() === HttpMethod.HEAD