diff --git a/buildSrc/src/main/kotlin/jadx-java.gradle.kts b/buildSrc/src/main/kotlin/jadx-java.gradle.kts index 974b9cf8..844befbd 100644 --- a/buildSrc/src/main/kotlin/jadx-java.gradle.kts +++ b/buildSrc/src/main/kotlin/jadx-java.gradle.kts @@ -18,7 +18,6 @@ dependencies { testImplementation("ch.qos.logback:logback-classic:1.5.9") testImplementation("org.assertj:assertj-core:3.26.3") - testImplementation("org.mockito:mockito-core:5.14.1") testImplementation("org.junit.jupiter:junit-jupiter:5.11.2") testRuntimeOnly("org.junit.platform:junit-platform-launcher") diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLI.java b/jadx-cli/src/main/java/jadx/cli/JadxCLI.java index 98131417..35b62ff0 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLI.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLI.java @@ -1,5 +1,7 @@ package jadx.cli; +import java.util.Set; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -8,8 +10,11 @@ import jadx.api.JadxDecompiler; import jadx.api.impl.AnnotatedCodeWriter; import jadx.api.impl.NoOpCodeCache; import jadx.api.impl.SimpleCodeWriter; +import jadx.api.security.JadxSecurityFlag; +import jadx.api.security.impl.JadxSecurity; import jadx.cli.LogHelper.LogLevelEnum; import jadx.cli.plugins.JadxFilesGetter; +import jadx.commons.app.JadxCommonEnv; import jadx.core.utils.exceptions.JadxArgsValidateException; import jadx.core.utils.files.FileUtils; import jadx.plugins.tools.JadxExternalPluginsLoader; @@ -49,6 +54,7 @@ public class JadxCLI { jadxArgs.setPluginLoader(new JadxExternalPluginsLoader()); jadxArgs.setFilesGetter(JadxFilesGetter.INSTANCE); initCodeWriterProvider(jadxArgs); + applyEnvVars(jadxArgs); try (JadxDecompiler jadx = new JadxDecompiler(jadxArgs)) { jadx.load(); if (checkForErrors(jadx)) { @@ -81,6 +87,22 @@ public class JadxCLI { } } + private static void applyEnvVars(JadxArgs jadxArgs) { + Set flags = JadxSecurityFlag.all(); + boolean modified = false; + boolean disableXmlSecurity = JadxCommonEnv.getBool("JADX_DISABLE_XML_SECURITY", false); + if (disableXmlSecurity) { + flags.remove(JadxSecurityFlag.SECURE_XML_PARSER); + // TODO: not related to 'xml security', but kept for compatibility + flags.remove(JadxSecurityFlag.VERIFY_APP_PACKAGE); + modified = true; + } + // TODO: migrate 'ZipSecurity' + if (modified) { + jadxArgs.setSecurity(new JadxSecurity(flags)); + } + } + private static boolean checkForErrors(JadxDecompiler jadx) { if (jadx.getRoot().getClasses().isEmpty()) { if (jadx.getArgs().isSkipResources()) { diff --git a/jadx-core/src/main/java/jadx/api/JadxArgs.java b/jadx-core/src/main/java/jadx/api/JadxArgs.java index 1d38123a..18fd5e46 100644 --- a/jadx-core/src/main/java/jadx/api/JadxArgs.java +++ b/jadx-core/src/main/java/jadx/api/JadxArgs.java @@ -30,6 +30,9 @@ import jadx.api.impl.AnnotatedCodeWriter; import jadx.api.impl.InMemoryCodeCache; import jadx.api.plugins.loader.JadxBasePluginLoader; import jadx.api.plugins.loader.JadxPluginLoader; +import jadx.api.security.IJadxSecurity; +import jadx.api.security.JadxSecurityFlag; +import jadx.api.security.impl.JadxSecurity; import jadx.api.usage.IUsageInfoCache; import jadx.api.usage.impl.InMemoryUsageInfoCache; import jadx.core.deobf.DeobfAliasProvider; @@ -174,6 +177,11 @@ public class JadxArgs implements Closeable { */ private IJadxFilesGetter filesGetter = TempFilesGetter.INSTANCE; + /** + * Additional data validation and security checks + */ + private IJadxSecurity security = new JadxSecurity(JadxSecurityFlag.all()); + /** * Don't save files (can be using for performance testing) */ @@ -726,6 +734,14 @@ public class JadxArgs implements Closeable { this.filesGetter = filesGetter; } + public IJadxSecurity getSecurity() { + return security; + } + + public void setSecurity(IJadxSecurity security) { + this.security = security; + } + public boolean isSkipFilesSave() { return skipFilesSave; } diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 39678d2e..b0b2c755 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -51,7 +51,6 @@ import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; import jadx.core.utils.tasks.TaskExecutor; -import jadx.core.xmlgen.BinaryXMLParser; import jadx.core.xmlgen.ResourcesSaver; /** @@ -92,8 +91,6 @@ public final class JadxDecompiler implements Closeable { private List classes; private List resources; - private BinaryXMLParser binaryXmlParser; - private final IDecompileScheduler decompileScheduler = new DecompilerScheduler(); private final JadxEventsImpl events = new JadxEventsImpl(); private final ResourcesLoader resourcesLoader = new ResourcesLoader(this); @@ -168,7 +165,6 @@ public final class JadxDecompiler implements Closeable { root = null; classes = null; resources = null; - binaryXmlParser = null; events.reset(); } @@ -467,13 +463,6 @@ public final class JadxDecompiler implements Closeable { return root; } - synchronized BinaryXMLParser getBinaryXmlParser() { - if (binaryXmlParser == null) { - binaryXmlParser = new BinaryXMLParser(root); - } - return binaryXmlParser; - } - /** * Get JavaClass by ClassNode without loading and decompilation */ diff --git a/jadx-core/src/main/java/jadx/api/security/IJadxSecurity.java b/jadx-core/src/main/java/jadx/api/security/IJadxSecurity.java new file mode 100644 index 00000000..a3bd5f97 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/security/IJadxSecurity.java @@ -0,0 +1,20 @@ +package jadx.api.security; + +import java.io.InputStream; + +import org.w3c.dom.Document; + +public interface IJadxSecurity { + + /** + * Check if application package is safe + * + * @return normalized/sanitized string or same string if safe + */ + String verifyAppPackage(String appPackage); + + /** + * XML document parser + */ + Document parseXml(InputStream in); +} diff --git a/jadx-core/src/main/java/jadx/api/security/JadxSecurityFlag.java b/jadx-core/src/main/java/jadx/api/security/JadxSecurityFlag.java new file mode 100644 index 00000000..1073453d --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/security/JadxSecurityFlag.java @@ -0,0 +1,18 @@ +package jadx.api.security; + +import java.util.EnumSet; +import java.util.Set; + +public enum JadxSecurityFlag { + + VERIFY_APP_PACKAGE, + SECURE_XML_PARSER; + + public static Set all() { + return EnumSet.allOf(JadxSecurityFlag.class); + } + + public static Set none() { + return EnumSet.noneOf(JadxSecurityFlag.class); + } +} diff --git a/jadx-core/src/main/java/jadx/api/security/impl/JadxSecurity.java b/jadx-core/src/main/java/jadx/api/security/impl/JadxSecurity.java new file mode 100644 index 00000000..ed412319 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/security/impl/JadxSecurity.java @@ -0,0 +1,73 @@ +package jadx.api.security.impl; + +import java.io.InputStream; +import java.util.Set; + +import javax.xml.parsers.DocumentBuilderFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; + +import jadx.api.security.IJadxSecurity; +import jadx.api.security.JadxSecurityFlag; +import jadx.core.deobf.NameMapper; + +public class JadxSecurity implements IJadxSecurity { + private static final Logger LOG = LoggerFactory.getLogger(JadxSecurity.class); + + private final Set flags; + + public JadxSecurity(Set flags) { + this.flags = flags; + } + + @Override + public String verifyAppPackage(String appPackage) { + if (flags.contains(JadxSecurityFlag.VERIFY_APP_PACKAGE) + && !NameMapper.isValidFullIdentifier(appPackage)) { + LOG.warn("App package '{}' has invalid format and will be ignored", appPackage); + return "INVALID_PACKAGE"; + } + return appPackage; + } + + @Override + public Document parseXml(InputStream in) { + DocumentBuilderFactory dbf; + if (flags.contains(JadxSecurityFlag.SECURE_XML_PARSER)) { + dbf = SecureDBFHolder.INSTANCE; + } else { + dbf = SimpleDBFHolder.INSTANCE; + } + try { + return dbf.newDocumentBuilder().parse(in); + } catch (Exception e) { + throw new RuntimeException("Failed to parse xml", e); + } + } + + private static final class SimpleDBFHolder { + private static final DocumentBuilderFactory INSTANCE = DocumentBuilderFactory.newInstance(); + } + + private static final class SecureDBFHolder { + private static final DocumentBuilderFactory INSTANCE = buildSecureDBF(); + + private static DocumentBuilderFactory buildSecureDBF() { + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); + dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + dbf.setFeature("http://apache.org/xml/features/dom/create-entity-ref-nodes", false); + dbf.setXIncludeAware(false); + dbf.setExpandEntityReferences(false); + return dbf; + } catch (Exception e) { + throw new RuntimeException("Fail to build secure XML DocumentBuilderFactory", e); + } + } + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java index b371ae44..776fecce 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java @@ -98,6 +98,8 @@ public class RootNode { */ private @Nullable JadxDecompiler decompiler; + private @Nullable ManifestAttributes manifestAttributes; + public RootNode(JadxArgs args) { this.args = args; this.preDecompilePasses = Jadx.getPreDecompilePassesList(); @@ -211,7 +213,7 @@ public class RootNode { if (parser != null) { processResources(parser.getResStorage()); updateObfuscatedFiles(parser, resources); - ManifestAttributes.getInstance().updateAttributes(parser); + initManifestAttributes().updateAttributes(parser); } } catch (Exception e) { LOG.error("Failed to parse 'resources.pb'/'.arsc' file", e); @@ -721,4 +723,13 @@ public class RootNode { public GradleInfoStorage getGradleInfoStorage() { return gradleInfoStorage; } + + public synchronized ManifestAttributes initManifestAttributes() { + ManifestAttributes attrs = manifestAttributes; + if (attrs == null) { + attrs = new ManifestAttributes(args.getSecurity()); + manifestAttributes = attrs; + } + return attrs; + } } diff --git a/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java b/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java index 345a8cbb..d5e24735 100644 --- a/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java +++ b/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java @@ -95,7 +95,8 @@ public class ExportGradleProject { AppAttribute.MIN_SDK_VERSION, AppAttribute.TARGET_SDK_VERSION, AppAttribute.VERSION_CODE, - AppAttribute.VERSION_NAME)); + AppAttribute.VERSION_NAME), + root.getArgs().getSecurity()); return parser.parse(); } } diff --git a/jadx-core/src/main/java/jadx/core/utils/Utils.java b/jadx-core/src/main/java/jadx/core/utils/Utils.java index acdb1ada..319d4e3e 100644 --- a/jadx-core/src/main/java/jadx/core/utils/Utils.java +++ b/jadx-core/src/main/java/jadx/core/utils/Utils.java @@ -495,6 +495,11 @@ public class Utils { } } + /** + * @deprecated env vars shouldn't be used in core modules. + * Prefer to parse in `app` (use JadxCommonEnv from 'app-commons') and set in jadx args. + */ + @Deprecated public static boolean getEnvVarBool(String varName, boolean defValue) { String strValue = System.getenv(varName); if (strValue == null) { @@ -503,6 +508,11 @@ public class Utils { return strValue.equalsIgnoreCase("true"); } + /** + * @deprecated env vars shouldn't be used in core modules. + * Prefer to parse in `app` (use JadxCommonEnv from 'app-commons') and set in jadx args. + */ + @Deprecated public static int getEnvVarInt(String varName, int defValue) { String strValue = System.getenv(varName); if (strValue == null) { diff --git a/jadx-core/src/main/java/jadx/core/utils/android/AndroidManifestParser.java b/jadx-core/src/main/java/jadx/core/utils/android/AndroidManifestParser.java index 9276669a..b9cfeaf6 100644 --- a/jadx-core/src/main/java/jadx/core/utils/android/AndroidManifestParser.java +++ b/jadx-core/src/main/java/jadx/core/utils/android/AndroidManifestParser.java @@ -1,38 +1,37 @@ package jadx.core.utils.android; -import java.io.StringReader; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.EnumSet; import java.util.List; - -import javax.xml.parsers.DocumentBuilder; +import java.util.Objects; import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; import jadx.api.ResourceFile; import jadx.api.ResourceType; +import jadx.api.security.IJadxSecurity; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.xmlgen.ResContainer; -import jadx.core.xmlgen.XmlSecurity; public class AndroidManifestParser { - private static final Logger LOG = LoggerFactory.getLogger(AndroidManifestParser.class); - private final Document androidManifest; private final Document appStrings; private final EnumSet parseAttrs; + private final IJadxSecurity security; - public AndroidManifestParser(ResourceFile androidManifestRes, EnumSet parseAttrs) { - this(androidManifestRes, null, parseAttrs); + public AndroidManifestParser(ResourceFile androidManifestRes, EnumSet parseAttrs, IJadxSecurity security) { + this(androidManifestRes, null, parseAttrs, security); } - public AndroidManifestParser(ResourceFile androidManifestRes, ResContainer appStrings, EnumSet parseAttrs) { + public AndroidManifestParser(ResourceFile androidManifestRes, ResContainer appStrings, + EnumSet parseAttrs, IJadxSecurity security) { this.parseAttrs = parseAttrs; + this.security = Objects.requireNonNull(security); this.androidManifest = parseAndroidManifest(androidManifestRes); this.appStrings = parseAppStrings(appStrings); @@ -206,10 +205,9 @@ public class AndroidManifestParser { return false; } - private static Document parseXml(String xmlContent) { - try { - DocumentBuilder builder = XmlSecurity.getDBF().newDocumentBuilder(); - Document document = builder.parse(new InputSource(new StringReader(xmlContent))); + private Document parseXml(String xmlContent) { + try (InputStream xmlStream = new ByteArrayInputStream(xmlContent.getBytes(StandardCharsets.UTF_8))) { + Document document = security.parseXml(xmlStream); document.getDocumentElement().normalize(); return document; } catch (Exception e) { @@ -217,7 +215,7 @@ public class AndroidManifestParser { } } - private static Document parseAppStrings(ResContainer appStrings) { + private Document parseAppStrings(ResContainer appStrings) { if (appStrings == null) { return null; } @@ -225,7 +223,7 @@ public class AndroidManifestParser { return parseXml(content); } - private static Document parseAndroidManifest(ResourceFile androidManifest) { + private Document parseAndroidManifest(ResourceFile androidManifest) { if (androidManifest == null) { return null; } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java b/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java index 5c7fec7d..39101215 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java @@ -23,21 +23,13 @@ import jadx.core.utils.android.AndroidResourcesMap; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.xmlgen.entry.ValuesParser; -/* - * TODO: - * Don't die when error occurs - * Check error cases, maybe checked const values are not always the same - * Better error messages - * What to do, when Binary XML Manifest is > size(int)? - * Check for missing chunk size types - * Implement missing data types - * Use line numbers to recreate EXACT AndroidManifest - * Check Element chunk size - */ - public class BinaryXMLParser extends CommonBinaryParser { private static final Logger LOG = LoggerFactory.getLogger(BinaryXMLParser.class); + private final RootNode rootNode; + private final ManifestAttributes manifestAttributes; + private final boolean attrNewLine; + private final Map resNames; private Map nsMap; private Set nsMapGenerated; @@ -53,16 +45,13 @@ public class BinaryXMLParser extends CommonBinaryParser { private boolean isOneLine = true; private int namespaceDepth = 0; private @Nullable int[] resourceIds; - - private final RootNode rootNode; private String appPackageName; private Map classNameCache; - private final boolean attrNewLine; - public BinaryXMLParser(RootNode rootNode) { this.rootNode = rootNode; + this.manifestAttributes = rootNode.initManifestAttributes(); this.attrNewLine = !rootNode.getArgs().isSkipXmlPrettyPrint(); try { ConstStorage constStorage = rootNode.getConstValues(); @@ -325,7 +314,7 @@ public class BinaryXMLParser extends CommonBinaryParser { writer.add(' '); } writer.add(attrFullName).add("=\""); - String decodedAttr = ManifestAttributes.getInstance().decode(attrName, attrValData); + String decodedAttr = manifestAttributes.decode(attrName, attrValData); if (decodedAttr != null) { memorizePackageName(attrName, decodedAttr); if (isDeobfCandidateAttr(attrFullName)) { @@ -419,8 +408,7 @@ public class BinaryXMLParser extends CommonBinaryParser { return "NOT_FOUND_STR_0x" + Integer.toHexString(strId); } - private void decodeAttribute(int attributeNS, int attrValDataType, int attrValData, - String attrFullName) { + private void decodeAttribute(int attributeNS, int attrValDataType, int attrValData, String attrFullName) { if (attrValDataType == TYPE_REFERENCE) { // reference custom processing String resName = resNames.get(attrValData); diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ManifestAttributes.java b/jadx-core/src/main/java/jadx/core/xmlgen/ManifestAttributes.java index 8b30ac13..446c833e 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ManifestAttributes.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ManifestAttributes.java @@ -7,8 +7,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import javax.xml.parsers.DocumentBuilder; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; @@ -16,11 +14,17 @@ import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import jadx.api.security.IJadxSecurity; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.xmlgen.entry.RawNamedValue; import jadx.core.xmlgen.entry.ResourceEntry; import jadx.core.xmlgen.entry.ValuesParser; +// TODO: move to Android specific module! + +/** + * Load and store Android Manifest attributes specification + */ public class ManifestAttributes { private static final Logger LOG = LoggerFactory.getLogger(ManifestAttributes.class); @@ -53,24 +57,12 @@ public class ManifestAttributes { } } + private final IJadxSecurity security; private final Map attrMap = new HashMap<>(); - private final Map appAttrMap = new HashMap<>(); - private static ManifestAttributes instance; - - public static ManifestAttributes getInstance() { - if (instance == null) { - try { - instance = new ManifestAttributes(); - } catch (Exception e) { - LOG.error("Failed to create ManifestAttributes", e); - } - } - return instance; - } - - private ManifestAttributes() { + public ManifestAttributes(IJadxSecurity security) { + this.security = security; parseAll(); } @@ -86,8 +78,7 @@ public class ManifestAttributes { if (xmlStream == null) { throw new JadxRuntimeException(xml + " not found in classpath"); } - DocumentBuilder dBuilder = XmlSecurity.getDBF().newDocumentBuilder(); - doc = dBuilder.parse(xmlStream); + doc = security.parseXml(xmlStream); } catch (Exception e) { throw new JadxRuntimeException("Xml load error, file: " + xml, e); } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResTableBinaryParser.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResTableBinaryParser.java index 604c1988..1096cddd 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResTableBinaryParser.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResTableBinaryParser.java @@ -87,7 +87,7 @@ public class ResTableBinaryParser extends CommonBinaryParser implements IResTabl public void decode(InputStream inputStream) throws IOException { long start = System.currentTimeMillis(); is = new ParserStream(inputStream); - resStorage = new ResourceStorage(); + resStorage = new ResourceStorage(root.getArgs().getSecurity()); decodeTableChunk(); resStorage.finish(); if (LOG.isDebugEnabled()) { @@ -99,7 +99,7 @@ public class ResTableBinaryParser extends CommonBinaryParser implements IResTabl @Override public ResContainer decodeFiles() { ValuesParser vp = new ValuesParser(strings, resStorage.getResourcesNames()); - ResXmlGen resGen = new ResXmlGen(resStorage, vp); + ResXmlGen resGen = new ResXmlGen(resStorage, vp, root.initManifestAttributes()); ICodeInfo content = XmlGenUtils.makeXmlDump(root.makeCodeWriter(), resStorage); List xmlFiles = resGen.makeResourcesXml(root.getArgs()); diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java index 517262cb..6f770d31 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java @@ -43,10 +43,12 @@ public class ResXmlGen { private final ResourceStorage resStorage; private final ValuesParser vp; + private final ManifestAttributes manifestAttributes; - public ResXmlGen(ResourceStorage resStorage, ValuesParser vp) { + public ResXmlGen(ResourceStorage resStorage, ValuesParser vp, ManifestAttributes manifestAttributes) { this.resStorage = resStorage; this.vp = vp; + this.manifestAttributes = manifestAttributes; } public List makeResourcesXml(JadxArgs args) { @@ -191,7 +193,7 @@ public class ResXmlGen { if (dataType == ParserConstants.TYPE_INT_DEC && nameStr != null) { try { int intVal = Integer.parseInt(valueStr); - String newVal = ManifestAttributes.getInstance().decode(nameStr.replace("android:", "").replace("attr.", ""), intVal); + String newVal = manifestAttributes.decode(nameStr.replace("android:", "").replace("attr.", ""), intVal); if (newVal != null) { valueStr = newVal; } @@ -202,7 +204,7 @@ public class ResXmlGen { if (dataType == ParserConstants.TYPE_INT_HEX && nameStr != null) { try { int intVal = Integer.decode(valueStr); - String newVal = ManifestAttributes.getInstance().decode(nameStr.replace("android:", "").replace("attr.", ""), intVal); + String newVal = manifestAttributes.decode(nameStr.replace("android:", "").replace("attr.", ""), intVal); if (newVal != null) { valueStr = newVal; } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResourceStorage.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResourceStorage.java index 82d7509d..5360916b 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResourceStorage.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResourceStorage.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; +import jadx.api.security.IJadxSecurity; import jadx.core.xmlgen.entry.ResourceEntry; public class ResourceStorage { @@ -16,6 +17,8 @@ public class ResourceStorage { .thenComparing(ResourceEntry::getKeyName); private final List list = new ArrayList<>(); + private final IJadxSecurity security; + private String appPackage; /** @@ -28,6 +31,10 @@ public class ResourceStorage { */ private final Map renames = new HashMap<>(); + public ResourceStorage(IJadxSecurity security) { + this.security = security; + } + public void add(ResourceEntry resEntry) { list.add(resEntry); uniqNameEntries.put(resEntry, resEntry); @@ -76,7 +83,7 @@ public class ResourceStorage { } public void setAppPackage(String appPackage) { - this.appPackage = XmlSecurity.verifyAppPackage(appPackage); + this.appPackage = security.verifyAppPackage(appPackage); } public Map getResourcesNames() { diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/XmlSecurity.java b/jadx-core/src/main/java/jadx/core/xmlgen/XmlSecurity.java deleted file mode 100644 index 912e010e..00000000 --- a/jadx-core/src/main/java/jadx/core/xmlgen/XmlSecurity.java +++ /dev/null @@ -1,54 +0,0 @@ -package jadx.core.xmlgen; - -import javax.xml.parsers.DocumentBuilderFactory; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import jadx.core.deobf.NameMapper; -import jadx.core.utils.Utils; - -public class XmlSecurity { - private static final Logger LOG = LoggerFactory.getLogger(XmlSecurity.class); - - private static final boolean DISABLE_CHECKS = Utils.getEnvVarBool("JADX_DISABLE_XML_SECURITY", false); - - private static final DocumentBuilderFactory DBF_INSTANCE = buildDBF(); - - private XmlSecurity() { - } - - public static DocumentBuilderFactory getDBF() { - return DBF_INSTANCE; - } - - public static String verifyAppPackage(String appPackage) { - if (DISABLE_CHECKS) { - return appPackage; - } - if (NameMapper.isValidFullIdentifier(appPackage)) { - return appPackage; - } - LOG.warn("App package '{}' has invalid format and will be ignored", appPackage); - return "INVALID_PACKAGE"; - } - - private static DocumentBuilderFactory buildDBF() { - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - if (DISABLE_CHECKS) { - return dbf; - } - try { - dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); - dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); - dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); - dbf.setFeature("http://apache.org/xml/features/dom/create-entity-ref-nodes", false); - dbf.setXIncludeAware(false); - dbf.setExpandEntityReferences(false); - return dbf; - } catch (Exception e) { - throw new RuntimeException("Fail to build secure XML DocumentBuilderFactory", e); - } - } -} diff --git a/jadx-core/src/test/java/jadx/api/JadxDecompilerTestUtils.java b/jadx-core/src/test/java/jadx/api/JadxDecompilerTestUtils.java deleted file mode 100644 index dd268069..00000000 --- a/jadx-core/src/test/java/jadx/api/JadxDecompilerTestUtils.java +++ /dev/null @@ -1,17 +0,0 @@ -package jadx.api; - -import jadx.core.dex.nodes.RootNode; -import jadx.core.xmlgen.BinaryXMLParser; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class JadxDecompilerTestUtils { - public static JadxDecompiler getMockDecompiler() { - JadxDecompiler decompiler = mock(JadxDecompiler.class); - RootNode rootNode = new RootNode(new JadxArgs()); - when(decompiler.getRoot()).thenReturn(rootNode); - when(decompiler.getBinaryXmlParser()).thenReturn(new BinaryXMLParser(rootNode)); - return decompiler; - } -} diff --git a/jadx-core/src/test/java/jadx/core/xmlgen/ResXmlGenTest.java b/jadx-core/src/test/java/jadx/core/xmlgen/ResXmlGenTest.java index 29d37942..4ff65eac 100644 --- a/jadx-core/src/test/java/jadx/core/xmlgen/ResXmlGenTest.java +++ b/jadx-core/src/test/java/jadx/core/xmlgen/ResXmlGenTest.java @@ -8,6 +8,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import jadx.api.JadxArgs; +import jadx.api.security.IJadxSecurity; +import jadx.api.security.JadxSecurityFlag; +import jadx.api.security.impl.JadxSecurity; import jadx.core.xmlgen.entry.RawNamedValue; import jadx.core.xmlgen.entry.RawValue; import jadx.core.xmlgen.entry.ResourceEntry; @@ -17,6 +20,8 @@ import static org.assertj.core.api.Assertions.assertThat; class ResXmlGenTest { private final JadxArgs args = new JadxArgs(); + private final IJadxSecurity security = new JadxSecurity(JadxSecurityFlag.all()); + private final ManifestAttributes manifestAttributes = new ManifestAttributes(security); @BeforeEach void init() { @@ -25,13 +30,13 @@ class ResXmlGenTest { @Test void testSimpleAttr() { - ResourceStorage resStorage = new ResourceStorage(); + ResourceStorage resStorage = new ResourceStorage(security); ResourceEntry re = new ResourceEntry(2130903103, "jadx.gui.app", "attr", "size", ""); re.setNamedValues(Lists.list(new RawNamedValue(16777216, new RawValue(16, 64)))); resStorage.add(re); ValuesParser vp = new ValuesParser(null, resStorage.getResourcesNames()); - ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp); + ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp, manifestAttributes); List files = resXmlGen.makeResourcesXml(args); assertThat(files).hasSize(1); @@ -46,14 +51,14 @@ class ResXmlGenTest { @Test void testAttrEnum() { - ResourceStorage resStorage = new ResourceStorage(); + ResourceStorage resStorage = new ResourceStorage(security); ResourceEntry re = new ResourceEntry(2130903103, "jadx.gui.app", "attr", "size", ""); re.setNamedValues( Lists.list(new RawNamedValue(16777216, new RawValue(16, 65536)), new RawNamedValue(17039620, new RawValue(16, 1)))); resStorage.add(re); ValuesParser vp = new ValuesParser(null, resStorage.getResourcesNames()); - ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp); + ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp, manifestAttributes); List files = resXmlGen.makeResourcesXml(args); assertThat(files).hasSize(1); @@ -69,14 +74,14 @@ class ResXmlGenTest { @Test void testAttrFlag() { - ResourceStorage resStorage = new ResourceStorage(); + ResourceStorage resStorage = new ResourceStorage(security); ResourceEntry re = new ResourceEntry(2130903103, "jadx.gui.app", "attr", "size", ""); re.setNamedValues( Lists.list(new RawNamedValue(16777216, new RawValue(16, 131072)), new RawNamedValue(17039620, new RawValue(16, 1)))); resStorage.add(re); ValuesParser vp = new ValuesParser(null, resStorage.getResourcesNames()); - ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp); + ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp, manifestAttributes); List files = resXmlGen.makeResourcesXml(args); assertThat(files).hasSize(1); @@ -92,14 +97,14 @@ class ResXmlGenTest { @Test void testAttrMin() { - ResourceStorage resStorage = new ResourceStorage(); + ResourceStorage resStorage = new ResourceStorage(security); ResourceEntry re = new ResourceEntry(2130903103, "jadx.gui.app", "attr", "size", ""); re.setNamedValues( Lists.list(new RawNamedValue(16777216, new RawValue(16, 4)), new RawNamedValue(16777217, new RawValue(16, 1)))); resStorage.add(re); ValuesParser vp = new ValuesParser(null, resStorage.getResourcesNames()); - ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp); + ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp, manifestAttributes); List files = resXmlGen.makeResourcesXml(args); assertThat(files).hasSize(1); @@ -114,7 +119,7 @@ class ResXmlGenTest { @Test void testStyle() { - ResourceStorage resStorage = new ResourceStorage(); + ResourceStorage resStorage = new ResourceStorage(security); ResourceEntry re = new ResourceEntry(2130903103, "jadx.gui.app", "style", "JadxGui", ""); re.setNamedValues(Lists.list(new RawNamedValue(16842836, new RawValue(1, 17170445)))); resStorage.add(re); @@ -124,7 +129,7 @@ class ResXmlGenTest { re.setNamedValues(new ArrayList<>()); resStorage.add(re); ValuesParser vp = new ValuesParser(null, resStorage.getResourcesNames()); - ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp); + ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp, manifestAttributes); List files = resXmlGen.makeResourcesXml(args); assertThat(files).hasSize(1); @@ -142,7 +147,7 @@ class ResXmlGenTest { @Test void testString() { - ResourceStorage resStorage = new ResourceStorage(); + ResourceStorage resStorage = new ResourceStorage(security); ResourceEntry re = new ResourceEntry(2130903103, "jadx.gui.app", "string", "app_name", ""); re.setSimpleValue(new RawValue(3, 0)); re.setNamedValues(Lists.list()); @@ -151,7 +156,7 @@ class ResXmlGenTest { BinaryXMLStrings strings = new BinaryXMLStrings(); strings.put(0, "Jadx Decompiler App"); ValuesParser vp = new ValuesParser(strings, resStorage.getResourcesNames()); - ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp); + ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp, manifestAttributes); List files = resXmlGen.makeResourcesXml(args); assertThat(files).hasSize(1); @@ -165,7 +170,7 @@ class ResXmlGenTest { @Test void testStringFormattedFalse() { - ResourceStorage resStorage = new ResourceStorage(); + ResourceStorage resStorage = new ResourceStorage(security); ResourceEntry re = new ResourceEntry(2130903103, "jadx.gui.app", "string", "app_name", ""); re.setSimpleValue(new RawValue(3, 0)); re.setNamedValues(Lists.list()); @@ -174,7 +179,7 @@ class ResXmlGenTest { BinaryXMLStrings strings = new BinaryXMLStrings(); strings.put(0, "%s at %s"); ValuesParser vp = new ValuesParser(strings, resStorage.getResourcesNames()); - ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp); + ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp, manifestAttributes); List files = resXmlGen.makeResourcesXml(args); assertThat(files).hasSize(1); @@ -188,7 +193,7 @@ class ResXmlGenTest { @Test void testArrayEscape() { - ResourceStorage resStorage = new ResourceStorage(); + ResourceStorage resStorage = new ResourceStorage(security); ResourceEntry re = new ResourceEntry(2130903103, "jadx.gui.app", "array", "single_quote_escape_sample", ""); re.setNamedValues( Lists.list(new RawNamedValue(16777216, new RawValue(3, 0)))); @@ -197,7 +202,7 @@ class ResXmlGenTest { BinaryXMLStrings strings = new BinaryXMLStrings(); strings.put(0, "Let's go"); ValuesParser vp = new ValuesParser(strings, resStorage.getResourcesNames()); - ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp); + ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp, manifestAttributes); List files = resXmlGen.makeResourcesXml(args); assertThat(files).hasSize(1); diff --git a/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java b/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java index c880c3ab..50395174 100644 --- a/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java @@ -2,58 +2,44 @@ package jadx.tests.api; import java.io.File; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.List; -import java.util.stream.Stream; import org.junit.jupiter.api.io.TempDir; import jadx.api.ICodeInfo; -import jadx.api.JadxDecompiler; -import jadx.api.JadxDecompilerTestUtils; +import jadx.api.JadxArgs; import jadx.api.ResourceFile; +import jadx.api.ResourceFileContent; +import jadx.api.ResourceType; +import jadx.api.impl.SimpleCodeInfo; import jadx.core.dex.nodes.RootNode; import jadx.core.export.ExportGradleProject; import jadx.core.export.ExportGradleTask; import jadx.core.xmlgen.ResContainer; -import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public abstract class ExportGradleTest { - - private final RootNode root; - - public ExportGradleTest() { - final JadxDecompiler decompiler = JadxDecompilerTestUtils.getMockDecompiler(); - root = decompiler.getRoot(); - } - private static final String MANIFEST_TESTS_DIR = "src/test/manifest"; + private final RootNode root = new RootNode(new JadxArgs()); + @TempDir private File exportDir; - protected ResContainer createResourceContainer(String filename) { - final ResContainer container = mock(ResContainer.class); - ICodeInfo codeInfo = mock(ICodeInfo.class); - when(codeInfo.getCodeStr()).thenReturn(loadFileContent(new File(MANIFEST_TESTS_DIR, filename))); - when(container.getText()).thenReturn(codeInfo); - return container; + protected ICodeInfo loadResource(String filename) { + return new SimpleCodeInfo(loadFileContent(new File(MANIFEST_TESTS_DIR, filename))); } private static String loadFileContent(File filePath) { - StringBuilder contentBuilder = new StringBuilder(); - - try (Stream stream = Files.lines(filePath.toPath(), StandardCharsets.UTF_8)) { - stream.forEach(s -> contentBuilder.append(s).append("\n")); + try { + return Files.readString(filePath.toPath()); } catch (IOException e) { - fail("Loading file failed: %s", e.getMessage()); + fail("Loading file failed", e); + return ""; } - return contentBuilder.toString(); } protected RootNode getRootNode() { @@ -61,30 +47,24 @@ public abstract class ExportGradleTest { } protected void exportGradle(String manifestFilename, String stringsFileName) { - ResourceFile androidManifest = mock(ResourceFile.class); - final ResContainer androidManifestContainer = createResourceContainer(manifestFilename); - when(androidManifest.loadContent()).thenReturn(androidManifestContainer); - final ResContainer strings = createResourceContainer(stringsFileName); + ResourceFile androidManifest = new ResourceFileContent(manifestFilename, + ResourceType.XML, loadResource(manifestFilename)); + ResContainer strings = ResContainer.textResource(stringsFileName, loadResource(stringsFileName)); - final ExportGradleTask exportGradleTask = new ExportGradleTask(List.of(androidManifest), root, exportDir); + ExportGradleTask exportGradleTask = new ExportGradleTask(List.of(androidManifest), root, exportDir); exportGradleTask.init(); assertThat(exportGradleTask.getSrcOutDir()).exists(); assertThat(exportGradleTask.getResOutDir()).exists(); - final ExportGradleProject export = - new ExportGradleProject(root, exportDir, androidManifest, strings); + ExportGradleProject export = new ExportGradleProject(root, exportDir, androidManifest, strings); export.generateGradleFiles(); } protected String getAppGradleBuild() { - File appBuildGradle = new File(exportDir, "app/build.gradle"); - assertThat(appBuildGradle).exists(); - return loadFileContent(appBuildGradle); + return loadFileContent(new File(exportDir, "app/build.gradle")); } protected String getSettingsGradle() { - File settingsGradle = new File(exportDir, "settings.gradle"); - assertThat(settingsGradle).exists(); - return loadFileContent(settingsGradle); + return loadFileContent(new File(exportDir, "settings.gradle")); } } diff --git a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java index a311b494..16497a5f 100644 --- a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java @@ -342,7 +342,7 @@ public abstract class IntegrationTest extends TestUtils { if (resMap.isEmpty()) { return; } - ResourceStorage resStorage = new ResourceStorage(); + ResourceStorage resStorage = new ResourceStorage(root.getArgs().getSecurity()); for (Map.Entry entry : resMap.entrySet()) { Integer id = entry.getKey(); String name = entry.getValue(); diff --git a/jadx-gui/src/main/java/jadx/gui/device/debugger/DbgUtils.java b/jadx-gui/src/main/java/jadx/gui/device/debugger/DbgUtils.java index 6f1bad5e..9639e61a 100644 --- a/jadx-gui/src/main/java/jadx/gui/device/debugger/DbgUtils.java +++ b/jadx-gui/src/main/java/jadx/gui/device/debugger/DbgUtils.java @@ -148,7 +148,8 @@ public class DbgUtils { } AndroidManifestParser parser = new AndroidManifestParser( AndroidManifestParser.getAndroidManifest(decompiler.getResources()), - EnumSet.of(AppAttribute.MAIN_ACTIVITY)); + EnumSet.of(AppAttribute.MAIN_ACTIVITY), + decompiler.getArgs().getSecurity()); if (!parser.isManifestFound()) { UiUtils.errorMessage(mw, NLS.str("error_dialog.not_found", "AndroidManifest.xml")); return null; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 4133c45c..cc6c8db1 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -952,7 +952,8 @@ public class MainWindow extends JFrame { public void goToMainActivity() { AndroidManifestParser parser = new AndroidManifestParser( AndroidManifestParser.getAndroidManifest(getWrapper().getResources()), - EnumSet.of(AppAttribute.MAIN_ACTIVITY)); + EnumSet.of(AppAttribute.MAIN_ACTIVITY), + getWrapper().getArgs().getSecurity()); if (!parser.isManifestFound()) { JOptionPane.showMessageDialog(MainWindow.this, NLS.str("error_dialog.not_found", "AndroidManifest.xml"), @@ -982,7 +983,8 @@ public class MainWindow extends JFrame { public void goToApplication() { AndroidManifestParser parser = new AndroidManifestParser( AndroidManifestParser.getAndroidManifest(getWrapper().getResources()), - EnumSet.of(AppAttribute.APPLICATION)); + EnumSet.of(AppAttribute.APPLICATION), + getWrapper().getArgs().getSecurity()); if (!parser.isManifestFound()) { JOptionPane.showMessageDialog(MainWindow.this, NLS.str("error_dialog.not_found", "AndroidManifest.xml"), diff --git a/jadx-plugins/jadx-aab-input/src/main/java/jadx/plugins/input/aab/parsers/ResTableProtoParser.java b/jadx-plugins/jadx-aab-input/src/main/java/jadx/plugins/input/aab/parsers/ResTableProtoParser.java index 8d1f4657..af42926c 100644 --- a/jadx-plugins/jadx-aab-input/src/main/java/jadx/plugins/input/aab/parsers/ResTableProtoParser.java +++ b/jadx-plugins/jadx-aab-input/src/main/java/jadx/plugins/input/aab/parsers/ResTableProtoParser.java @@ -34,7 +34,7 @@ public class ResTableProtoParser extends CommonProtoParser implements IResTableP @Override public void decode(InputStream inputStream) throws IOException { - resStorage = new ResourceStorage(); + resStorage = new ResourceStorage(root.getArgs().getSecurity()); ResourceTable table = ResourceTable.parseFrom(FileUtils.streamToByteArray(inputStream)); for (Package p : table.getPackageList()) { parse(p); @@ -45,7 +45,7 @@ public class ResTableProtoParser extends CommonProtoParser implements IResTableP @Override public synchronized ResContainer decodeFiles() { ValuesParser vp = new ValuesParser(new BinaryXMLStrings(), resStorage.getResourcesNames()); - ResXmlGen resGen = new ResXmlGen(resStorage, vp); + ResXmlGen resGen = new ResXmlGen(resStorage, vp, root.initManifestAttributes()); ICodeInfo content = XmlGenUtils.makeXmlDump(root.makeCodeWriter(), resStorage); List xmlFiles = resGen.makeResourcesXml(root.getArgs()); return ResContainer.resourceTable("res", xmlFiles, content);