Loading src/org/lineageos/updater/UpdatesCheckReceiver.java +14 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); Loading @@ -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() { Loading @@ -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); Loading src/org/lineageos/updater/misc/JsonValidator.java 0 → 100644 +164 −0 Original line number Diff line number Diff line 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<String> 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<String> missingRequiredFields = new ArrayList<>(); List<String> 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; } } } src/org/lineageos/updater/misc/Utils.java +15 −0 Original line number Diff line number Diff line Loading @@ -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); Loading Loading @@ -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; } } } Loading
src/org/lineageos/updater/UpdatesCheckReceiver.java +14 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); Loading @@ -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() { Loading @@ -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); Loading
src/org/lineageos/updater/misc/JsonValidator.java 0 → 100644 +164 −0 Original line number Diff line number Diff line 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<String> 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<String> missingRequiredFields = new ArrayList<>(); List<String> 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; } } }
src/org/lineageos/updater/misc/Utils.java +15 −0 Original line number Diff line number Diff line Loading @@ -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); Loading Loading @@ -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; } } }