diff --git a/src/org/lineageos/updater/UpdatesCheckReceiver.java b/src/org/lineageos/updater/UpdatesCheckReceiver.java index 9ac37a25ff67ea389c0ee35d0c4ee8f35a59b89d..ad83d289f6e65137d44da0538be4415f056a95d0 100644 --- a/src/org/lineageos/updater/UpdatesCheckReceiver.java +++ b/src/org/lineageos/updater/UpdatesCheckReceiver.java @@ -33,6 +33,7 @@ import androidx.preference.PreferenceManager; import org.json.JSONException; import org.lineageos.updater.download.DownloadClient; import org.lineageos.updater.misc.Constants; +import org.lineageos.updater.misc.JsonValidator; import org.lineageos.updater.misc.Utils; import java.io.File; @@ -67,6 +68,13 @@ public class UpdatesCheckReceiver extends BroadcastReceiver { Utils.cleanupDownloadsDir(context); } + final File json = Utils.getCachedUpdateList(context); + boolean isValidatedJsonFile = JsonValidator.validateJsonFile(json); + if (!isValidatedJsonFile && json.exists()) { + Log.i(TAG, "Removing cached json file due validation failure"); + json.delete(); + } + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); @@ -85,7 +93,6 @@ public class UpdatesCheckReceiver extends BroadcastReceiver { return; } - final File json = Utils.getCachedUpdateList(context); final File jsonNew = new File(json.getAbsolutePath() + UUID.randomUUID()); String url = Utils.getServerURL(context); DownloadClient.DownloadCallback callback = new DownloadClient.DownloadCallback() { @@ -103,6 +110,12 @@ public class UpdatesCheckReceiver extends BroadcastReceiver { @Override public void onSuccess(File destination) { try { + boolean isValidated = JsonValidator.validateJsonFile(jsonNew); + if (!isValidated) { + Log.i(TAG, "Could not parse list, scheduling new check"); + scheduleUpdatesCheck(context); + return; + } if (json.exists() && Utils.checkForNewUpdates(jsonNew)) { showNotification(context); updateRepeatingUpdatesCheck(context); diff --git a/src/org/lineageos/updater/misc/JsonValidator.java b/src/org/lineageos/updater/misc/JsonValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..3e3bcc3aa66c8bccf642ea62c8323ef31203b8d7 --- /dev/null +++ b/src/org/lineageos/updater/misc/JsonValidator.java @@ -0,0 +1,164 @@ +package org.lineageos.updater.misc; + +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class JsonValidator { + + private static final String TAG = "JsonValidator" ; + private static final String[] jsonMainFields = { + "error", + "id", + "response" + }; + private static final String[] requiredFields = { + "api_level", + "url", + "timestamp", + "md5sum", + "channel", + "filename", + "romtype", + "version", + "display_version", + "android_version", + "id" + }; + private static final String[] optionalFields = { + "changes", + "pre_version" + }; + + public static boolean validateJsonFile(File jsonFile) { + try { + if (!jsonFile.exists()) { + Log.i(TAG, "Unable to locate json file"); + return false; + } + + // Read the JSON data from the file + BufferedReader reader = new BufferedReader(new FileReader(jsonFile)); + StringBuilder jsonString = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + jsonString.append(line); + } + reader.close(); + + List missingRequiredFields = new ArrayList<>(); + JSONObject jsonObject = new JSONObject(jsonString.toString()); + for (String field : jsonMainFields) { + if (!isMainFieldValid(jsonObject, field)) { + missingRequiredFields.add(field); + } + } + + if (!missingRequiredFields.isEmpty()) { + Log.i(TAG, "Missing or invalid required field in response object: " + + missingRequiredFields); + return false; + } + } catch (IOException | JSONException e) { + Log.i(TAG, "Unable to parse the json file:" + e); + } + return false; + } + + public static boolean validateResponseObject(JSONObject responseObject) { + List missingRequiredFields = new ArrayList<>(); + List missingOptionalFields = new ArrayList<>(); + + for (String field : requiredFields) { + if (!isObjectFieldValid(responseObject, field)) { + missingRequiredFields.add(field); + } + } + + for (String field : optionalFields) { + if (!isObjectFieldValid(responseObject, field)) { + missingOptionalFields.add(field); + } + } + + String filename = responseObject.optString("filename", ""); + if (!missingRequiredFields.isEmpty()) { + if (!filename.isEmpty()) { + Log.i(TAG, "Missing or invalid required field in response object for " + + filename + ": " + missingRequiredFields); + } + return false; + } else if (!missingOptionalFields.isEmpty() && !filename.isEmpty()) { + Log.i(TAG, "Missing or invalid optional field in response object for " + + filename + ": " + missingOptionalFields); + } + + return true; // All required fields are present and valid + } + + private static boolean isMainFieldValid(JSONObject jsonObject, String field) { + if (!jsonObject.has(field) || jsonObject.isNull(field)) { + return false; + } + + String value = jsonObject.optString(field, ""); // Return empty string if field doesn't exist + + switch (field) { + case "id": + case "error": + // Allow null, as it's a valid value + return true; + case "response": + // Check if it's an array with a length >= 0 + return jsonObject.optJSONArray(field) != null + && jsonObject.optJSONArray(field).length() >= 0; + default: + return !value.isEmpty(); + } + } + + private static boolean isObjectFieldValid(JSONObject jsonObject, String field) { + if (!jsonObject.has(field) || jsonObject.isNull(field)) { + return false; + } + + String value = jsonObject.optString(field, ""); // Return empty string if field doesn't exist + if (value.isEmpty()) { + return false; + } + + switch (field) { + case "datetime": + case "timestamp": + case "size": + case "api_level": + return Utils.isInteger(value); + case "is_upgrade_supported": + return "true".equals(value) || "false".equals(value); + case "android_version": + // Check if it's a valid Android version number + return value.matches("^(\\d+)$|^(\\d+\\.\\d+)$|^(\\d+\\.\\d+\\.\\d+)$"); + case "md5sum": + // Check if it's a valid MD5 checksum + return value.matches("^[a-fA-F0-9]{32}$"); + case "filename": + return value.endsWith(".zip"); + case "id": + // Check if it's a valid SHA-256 checksum + return value.matches("^[a-fA-F0-9]{64}$"); + case "url": + // Check if it's a valid URL format + return value.startsWith("http://") || value.startsWith("https://"); + default: + return true; + } + } +} diff --git a/src/org/lineageos/updater/misc/Utils.java b/src/org/lineageos/updater/misc/Utils.java index 6423b13f19057efb561abee8a99b72e2a3bc6545..1aef4ec7114d84182de4ff428a3870d0d45d6a75 100644 --- a/src/org/lineageos/updater/misc/Utils.java +++ b/src/org/lineageos/updater/misc/Utils.java @@ -201,6 +201,12 @@ public class Utils { continue; } try { + boolean isValidated = JsonValidator.validateResponseObject( + updatesList.getJSONObject(i)); + if (!isValidated) { + Log.d(TAG, "Ignoring incompatible update"); + continue; + } UpdateInfo update = parseJsonUpdate(updatesList.getJSONObject(i)); if (!compatibleOnly || isCompatible(update)) { updates.add(update); @@ -512,4 +518,13 @@ public class Utils { context.getResources().getInteger(R.integer.battery_ok_percentage_discharging); return percent >= required; } + + public static boolean isInteger(String value) { + try { + Integer.parseInt(value); + return true; + } catch (NumberFormatException e) { + return false; + } + } }