From 706f58798ca90b13d2ef694590f662eea3807e4c Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Thu, 29 Nov 2018 13:58:48 +0100 Subject: [PATCH] ensure that sensitive information is not serialized (part 2) --- .../testSrc/DoNotStorePasswordTest.kt | 6 +- .../testSrc/xml/XmlSerializerTest.kt | 90 +++++++++++++++---- .../src/kdbx/ProtectedValue.kt | 2 +- .../configurationStore/BaseXmlOutputter.kt | 6 +- .../configurationStore/JbXmlOutputter.java | 44 +++++++-- 5 files changed, 120 insertions(+), 28 deletions(-) diff --git a/platform/configuration-store-impl/testSrc/DoNotStorePasswordTest.kt b/platform/configuration-store-impl/testSrc/DoNotStorePasswordTest.kt index 9aa91cd7e4d4..9344bd503717 100644 --- a/platform/configuration-store-impl/testSrc/DoNotStorePasswordTest.kt +++ b/platform/configuration-store-impl/testSrc/DoNotStorePasswordTest.kt @@ -1,4 +1,6 @@ // Copyright 2000-2018 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. +// Copyright 2000-2018 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. +// Copyright 2000-2018 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. import com.intellij.configurationStore.BaseXmlOutputter import com.intellij.openapi.application.ApplicationManager @@ -69,8 +71,8 @@ class DoNotStorePasswordTest { for (accessor in XmlSerializerUtil.getAccessors(clazz)) { val name = accessor.name - if (name.contains("password", ignoreCase = true) && !BaseXmlOutputter.isSavePasswordField(name)) { - System.out.println("${clazz.typeName}.${accessor.name}") + if (BaseXmlOutputter.isNameIndicatesSensitiveInformation(name)) { + throw RuntimeException("${clazz.typeName}.${accessor.name}") } else if (!accessor.valueClass.isPrimitive) { @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") diff --git a/platform/configuration-store-impl/testSrc/xml/XmlSerializerTest.kt b/platform/configuration-store-impl/testSrc/xml/XmlSerializerTest.kt index 1cd4ca08b134..936d28ace831 100644 --- a/platform/configuration-store-impl/testSrc/xml/XmlSerializerTest.kt +++ b/platform/configuration-store-impl/testSrc/xml/XmlSerializerTest.kt @@ -3,10 +3,7 @@ package com.intellij.configurationStore.xml -import com.intellij.configurationStore.StoredPropertyStateTest -import com.intellij.configurationStore.clearBindingCache -import com.intellij.configurationStore.deserialize -import com.intellij.configurationStore.serialize +import com.intellij.configurationStore.* import com.intellij.openapi.util.text.StringUtil import com.intellij.testFramework.UsefulTestCase import com.intellij.testFramework.assertConcurrent @@ -14,12 +11,15 @@ import com.intellij.testFramework.assertions.Assertions.assertThat import com.intellij.util.loadElement import com.intellij.util.xmlb.* import com.intellij.util.xmlb.annotations.* +import com.intellij.util.xmlb.annotations.Property import junit.framework.TestCase +import org.assertj.core.api.Assertions.assertThatThrownBy import org.intellij.lang.annotations.Language import org.jdom.Element import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Suite +import java.io.StringWriter import java.util.* @RunWith(Suite::class) @@ -627,26 +627,80 @@ internal class XmlSerializerTest { testSerializer("", BeanWithDefaultAttributeName()) } - private class Bean2 { - @Attribute - var ab: String? = null + @Test + fun ordered() { + @Tag("bean") + class Bean { + @Attribute + var ab: String? = null - @Attribute - var module: String? = null + @Attribute + var module: String? = null - @Suppress("unused") - @Attribute - var ac: String? = null - } + @Suppress("unused") + @Attribute + var ac: String? = null + } - @Test fun ordered() { - val bean = Bean2() + val bean = Bean() bean.module = "module" bean.ab = "ab" - testSerializer("", bean, SkipDefaultsSerializationFilter()) + testSerializer("", bean, SkipDefaultsSerializationFilter()) } - @Test fun cdataAfterNewLine() { + @Test + fun `do not store password as attribute`() { + @Tag("bean") + class Bean { + @Attribute + var password: String? = null + + @Attribute + var foo: String? = null + } + + val bean = Bean() + bean.foo = "module" + bean.password = "ab" + // it is not part of XML bindings to ensure that even if you will use JDOM directly, you cannot output sensitive data + // so, testSerializer must not throw error + val element = assertSerializer(bean, "") + + assertThatThrownBy { + val xmlWriter = JbXmlOutputter(true) + xmlWriter.output(element, StringWriter()) + }.hasMessage("Attribute \"password\" probably contains sensitive information") + } + + @Test + fun `do not store password as element`() { + @Tag("bean") + class Bean { + var password: String? = null + + @Attribute + var foo: String? = null + } + + val bean = Bean() + bean.foo = "module" + bean.password = "ab" + // it is not part of XML bindings to ensure that even if you will use JDOM directly, you cannot output sensitive data + // so, testSerializer must not throw error + val element = assertSerializer(bean, """ + + + """.trimIndent()) + + assertThatThrownBy { + val xmlWriter = JbXmlOutputter(true) + xmlWriter.output(element, StringWriter()) + }.hasMessage("Element \"password\" probably contains sensitive information") + } + + @Test + fun cdataAfterNewLine() { @Tag("bean") data class Bean(@Tag val description: String? = null) @@ -687,7 +741,7 @@ internal class XmlSerializerTest { // } } -internal fun assertSerializer(bean: Any, expected: String, filter: SerializationFilter?, description: String = "Serialization failure"): Element { +internal fun assertSerializer(bean: Any, expected: String, filter: SerializationFilter? = null, description: String = "Serialization failure"): Element { val element = bean.serialize(filter, createElementIfEmpty = true)!! assertThat(element).`as`(description).isEqualTo(expected) return element diff --git a/platform/credential-store/src/kdbx/ProtectedValue.kt b/platform/credential-store/src/kdbx/ProtectedValue.kt index e62ce9a24488..9bad55928026 100644 --- a/platform/credential-store/src/kdbx/ProtectedValue.kt +++ b/platform/credential-store/src/kdbx/ProtectedValue.kt @@ -51,7 +51,7 @@ internal class UnsavedProtectedValue(val secureString: StringProtectedByStreamCi override fun getText() = throw IllegalStateException("Must be converted to ProtectedValue for serialization") } -internal class ProtectedXmlWriter(private val streamCipher: SkippingStreamCipher) : JbXmlOutputter("\n", null, null, null) { +internal class ProtectedXmlWriter(private val streamCipher: SkippingStreamCipher) : JbXmlOutputter(false) { override fun writeContent(out: Writer, element: Element, level: Int): Boolean { if (element.name == KdbxEntryElementNames.value) { val value = element.content.firstOrNull() diff --git a/platform/projectModel-impl/src/com/intellij/configurationStore/BaseXmlOutputter.kt b/platform/projectModel-impl/src/com/intellij/configurationStore/BaseXmlOutputter.kt index 942f455d0491..20f2b3179d74 100644 --- a/platform/projectModel-impl/src/com/intellij/configurationStore/BaseXmlOutputter.kt +++ b/platform/projectModel-impl/src/com/intellij/configurationStore/BaseXmlOutputter.kt @@ -8,7 +8,11 @@ import java.io.Writer abstract class BaseXmlOutputter(protected val lineSeparator: String) { companion object { - fun isSavePasswordField(name: String) = name.contains("remember", ignoreCase = true) || name.contains("keep", ignoreCase = true) || name.contains("save", ignoreCase = true) + fun isNameIndicatesSensitiveInformation(name: String): Boolean { + return name.contains("password") && !(name.contains("remember", ignoreCase = true) || + name.contains("keep", ignoreCase = true) || + name.contains("save", ignoreCase = true)) + } } /** diff --git a/platform/projectModel-impl/src/com/intellij/configurationStore/JbXmlOutputter.java b/platform/projectModel-impl/src/com/intellij/configurationStore/JbXmlOutputter.java index a1e1902ce6a7..23f6c49a74fe 100644 --- a/platform/projectModel-impl/src/com/intellij/configurationStore/JbXmlOutputter.java +++ b/platform/projectModel-impl/src/com/intellij/configurationStore/JbXmlOutputter.java @@ -9,6 +9,7 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.JDOMUtil; import com.intellij.openapi.util.SystemInfoRt; import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.xmlb.Constants; import org.jdom.*; import org.jdom.output.Format; import org.jetbrains.annotations.NotNull; @@ -35,21 +36,36 @@ public class JbXmlOutputter extends BaseXmlOutputter { @Nullable private final PathMacroFilter macroFilter; + private final boolean isForbidSensitiveData; + + public JbXmlOutputter(boolean isForbidSensitiveData) { + this("\n", null, null, null, isForbidSensitiveData); + } + public JbXmlOutputter(@NotNull String lineSeparator, + @Nullable JDOMUtil.ElementOutputFilter elementFilter, + @Nullable ReplacePathToMacroMap macroMap, + @Nullable PathMacroFilter macroFilter) { + this(lineSeparator, elementFilter, macroMap, macroFilter, true); + } + + private JbXmlOutputter(@NotNull String lineSeparator, @Nullable JDOMUtil.ElementOutputFilter elementFilter, @Nullable ReplacePathToMacroMap macroMap, - @Nullable PathMacroFilter macroFilter) { + @Nullable PathMacroFilter macroFilter, + boolean isForbidSensitiveData) { super(lineSeparator); this.format = DEFAULT_FORMAT.getLineSeparator().equals(lineSeparator) ? DEFAULT_FORMAT : JDOMUtil.createFormat(lineSeparator); this.elementFilter = elementFilter; this.macroMap = macroMap; this.macroFilter = macroFilter; + this.isForbidSensitiveData = isForbidSensitiveData; } public static void collapseMacrosAndWrite(@NotNull Element element, @NotNull ComponentManager project, @NotNull Writer writer) throws IOException { PathMacroManager macroManager = PathMacroManager.getInstance(project); - JbXmlOutputter xmlWriter = new JbXmlOutputter("\n", null, macroManager.getReplacePathMap(), macroManager.getMacroFilter()); + JbXmlOutputter xmlWriter = new JbXmlOutputter("\n", null, macroManager.getReplacePathMap(), macroManager.getMacroFilter(), true); xmlWriter.output(element, writer); } @@ -275,9 +291,25 @@ public class JbXmlOutputter extends BaseXmlOutputter { out.write('>'); } + private static void checkIsElementContainsSensitiveInformation(@NotNull Element element) { + String name = element.getName(); + if (BaseXmlOutputter.Companion.isNameIndicatesSensitiveInformation(name)) { + logSensitiveInformationError(name, "Element"); + } + + name = element.getAttributeValue(Constants.NAME); + if (name != null && BaseXmlOutputter.Companion.isNameIndicatesSensitiveInformation(name)) { + logSensitiveInformationError(name, "Element"); + } + } + + private static void logSensitiveInformationError(@NotNull String name, @NotNull String elementKind) { + Logger.getInstance(JbXmlOutputter.class).error(elementKind + " \"" + name + "\" probably contains sensitive information"); + } + protected boolean writeContent(@NotNull Writer out, @NotNull Element element, int level) throws IOException { - if (element.getName().contains("password") && !BaseXmlOutputter.Companion.isSavePasswordField(element.getName())) { - Logger.getInstance(JbXmlOutputter.class).error("Element " + element.getName() + " probably contains sensitive information"); + if (isForbidSensitiveData) { + checkIsElementContainsSensitiveInformation(element); } List content = element.getContent(); @@ -452,8 +484,8 @@ public class JbXmlOutputter extends BaseXmlOutputter { value = attribute.getValue(); } - if (attribute.getName().contains("password") && !BaseXmlOutputter.Companion.isSavePasswordField(attribute.getName())) { - Logger.getInstance(JbXmlOutputter.class).error("Attribute " + attribute.getName() + " probably contains sensitive information"); + if (isForbidSensitiveData && BaseXmlOutputter.Companion.isNameIndicatesSensitiveInformation(attribute.getName())) { + logSensitiveInformationError(attribute.getName(), "Attribute"); } out.write(escapeAttributeEntities(value));