30 days of bugs: Day 1

Welcome to "30 Days of Bugs," a campaign by 3Flatline, a startup that's all about making code safer. Our product, the Dixie Code Scanner, enabled by AI and machine learning (but also lots of normal and boring regular processes) to catch vulnerabilities that other tools miss. It doesn’t just find bugs—it also suggests fixes and writes test code to see if those vulnerabilities can be exploited. We're sharing a new bug every day from open-source projects to show just how powerful the Dixie Code Scanner is. These are real bugs that other tools didn't catch, highlighting that our scanner can make a real difference for developers and security researchers.

This is real output from our code scanner, minus the “tests” or code to help exploit the vulnerability (for obvious reasons). One day, we hope that we can stop prioritizing bugs based on CVEs and just automatically fix security vulnerabilities. Until then, we leverage the below output to reduce the amount of time you spend validating false positives or figuring out what your scanner is telling you.

And yes, for all you sneaky researchers out there: we have an offline version including LLM that can be run locally, or deploy into your tenant to keep you code secret and safe.

If you want to learn more, check out https://3flatline.ai or reach out to me here on LinkedIn. I’m happy to talk or give out free trial codes!

Today’s bug comes from . . .

XSS in NewPipe

https://github.com/TeamNewPipe/NewPipe

File Name:

? ? NewPipe/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java

Code Description:

The provided code defines a class named MissionAdapter, which is a custom adapter for a RecyclerView in an Android application. This adapter is responsible for managing and displaying a list of download missions, which can be in various states such as pending, finished, or in error. The adapter interacts with a DownloadManager to retrieve and update the status of these missions, allowing users to start, pause, retry, or delete downloads through a user interface.

The MissionAdapter utilizes a ViewHolder pattern to efficiently manage the views for each item in the list. It includes methods for creating and binding view holders, updating progress indicators, handling user interactions through popup menus, and displaying error messages when downloads fail. The adapter also manages visibility for empty messages and buttons based on the state of the download missions.

Key functionalities include:

  • Displaying the status and progress of each download mission.
  • Handling user actions such as starting, pausing, and deleting downloads.
  • Showing error messages and providing options to retry failed downloads.
  • Using RxJava for asynchronous operations related to checksum calculations.
  • Managing the visibility of UI elements based on the current state of downloads.

Overall, the MissionAdapter serves as a crucial component in the user interface for managing download tasks within the application.

Vulnerability Description: XSS

Summary:

h.name.setText(item.mission.storage.getName());        

The line of code sets the text of a TextView (h.name) directly from user-controlled input (item.mission.storage.getName()). If storage.getName() returns a string that contains HTML or JavaScript code, it can lead to Cross-Site Scripting (XSS) vulnerabilities. An attacker could manipulate the input to include malicious scripts, which would execute in the context of the application when the TextView is rendered. To mitigate this vulnerability, the input should be sanitized or escaped before being set to the TextView.

Assessment:

Report: True

Confidence Level: Medium confidence.

The report accurately identifies a potential XSS vulnerability due to the direct assignment of user-controlled input to a TextView without sanitization. However, the actual risk depends on the context in which the application is running and how the input is handled throughout the application. Further analysis of the input handling and rendering context would be necessary to fully assess the impact.

Fix:

Here is the remediated source code addressing the identified XSS vulnerability:

package us.shandian.giga.ui.adapter;

import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST;
import static us.shandian.giga.get.DownloadMission.ERROR_FILE_CREATION;
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_NO_CONTENT;
import static us.shandian.giga.get.DownloadMission.ERROR_INSUFFICIENT_STORAGE;
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
import static us.shandian.giga.get.DownloadMission.ERROR_PATH_CREATION;
import static us.shandian.giga.get.DownloadMission.ERROR_PERMISSION_DENIED;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED;
import static us.shandian.giga.get.DownloadMission.ERROR_PROGRESS_LOST;
import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE;
import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION;
import static us.shandian.giga.get.DownloadMission.ERROR_TIMEOUT;
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST;

import android.annotation.SuppressLint;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.MimeTypeMap;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.core.os.HandlerCompat;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.Adapter;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;

import com.google.android.material.snackbar.Snackbar;

import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;

import java.io.File;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;

import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.get.Mission;
import us.shandian.giga.get.MissionRecoveryInfo;
import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.ui.common.Deleter;
import us.shandian.giga.ui.common.ProgressDrawable;
import us.shandian.giga.util.Utility;

public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callback {
    private static final String TAG = "MissionAdapter";
    private static final String UNDEFINED_PROGRESS = "--.-%";
    private static final String DEFAULT_MIME_TYPE = "*/*";
    private static final String UNDEFINED_ETA = "--:--";

    private static final String UPDATER = "updater";
    private static final String DELETE = "deleteFinishedDownloads";

    private static final int HASH_NOTIFICATION_ID = 123790;

    private final Context mContext;
    private final LayoutInflater mInflater;
    private final DownloadManager mDownloadManager;
    private final Deleter mDeleter;
    private int mLayout;
    private final DownloadManager.MissionIterator mIterator;
    private final ArrayList<ViewHolderItem> mPendingDownloadsItems = new ArrayList<>();
    private final Handler mHandler;
    private MenuItem mClear;
    private MenuItem mStartButton;
    private MenuItem mPauseButton;
    private final View mEmptyMessage;
    private RecoverHelper mRecover;
    private final View mView;
    private final ArrayList<Mission> mHidden;
    private Snackbar mSnackbar;

    private final CompositeDisposable compositeDisposable = new CompositeDisposable();

    public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage, View root) {
        mContext = context;
        mDownloadManager = downloadManager;

        mInflater = LayoutInflater.from(mContext);
        mLayout = R.layout.mission_item;

        mHandler = new Handler(context.getMainLooper());

        mEmptyMessage = emptyMessage;

        mIterator = downloadManager.getIterator();

        mDeleter = new Deleter(root, mContext, this, mDownloadManager, mIterator, mHandler);

        mView = root;

        mHidden = new ArrayList<>();

        checkEmptyMessageVisibility();
        onResume();
    }

    @Override
    @NonNull
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        switch (viewType) {
            case DownloadManager.SPECIAL_PENDING:
            case DownloadManager.SPECIAL_FINISHED:
                return new ViewHolderHeader(mInflater.inflate(R.layout.missions_header, parent, false));
        }

        return new ViewHolderItem(mInflater.inflate(mLayout, parent, false));
    }

    @Override
    public void onViewRecycled(@NonNull ViewHolder view) {
        super.onViewRecycled(view);

        if (view instanceof ViewHolderHeader) return;
        ViewHolderItem h = (ViewHolderItem) view;

        if (h.item.mission instanceof DownloadMission) {
            mPendingDownloadsItems.remove(h);
            if (mPendingDownloadsItems.size() < 1) {
                checkMasterButtonsVisibility();
            }
        }

        h.popupMenu.dismiss();
        h.item = null;
        h.resetSpeedMeasure();
    }

    @Override
    @SuppressLint("SetTextI18n")
    public void onBindViewHolder(@NonNull ViewHolder view, @SuppressLint("RecyclerView") int pos) {
        DownloadManager.MissionItem item = mIterator.getItem(pos);

        if (view instanceof ViewHolderHeader) {
            if (item.special == DownloadManager.SPECIAL_NOTHING) return;
            int str;
            if (item.special == DownloadManager.SPECIAL_PENDING) {
                str = R.string.missions_header_pending;
            } else {
                str = R.string.missions_header_finished;
                if (mClear != null) mClear.setVisible(true);
            }

            ((ViewHolderHeader) view).header.setText(str);
            return;
        }

        ViewHolderItem h = (ViewHolderItem) view;
        h.item = item;

        Utility.FileType type = Utility.getFileType(item.mission.kind, item.mission.storage.getName());

        h.icon.setImageResource(Utility.getIconForFileType(type));
        
        // Sanitize the name to prevent XSS
        String sanitizedName = sanitizeInput(item.mission.storage.getName());
        h.name.setText(sanitizedName);

        h.progress.setColors(Utility.getBackgroundForFileType(mContext, type), Utility.getForegroundForFileType(mContext, type));

        if (h.item.mission instanceof DownloadMission) {
            DownloadMission mission = (DownloadMission) item.mission;
            String length = Utility.formatBytes(mission.getLength());
            if (mission.running && !mission.isPsRunning()) length += " --.- kB/s";

            h.size.setText(length);
            h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause);
            updateProgress(h);
            mPendingDownloadsItems.add(h);
        } else {
            h.progress.setMarquee(false);
            h.status.setText("100%");
            h.progress.setProgress(1.0f);
            h.size.setText(Utility.formatBytes(item.mission.length));
        }
    }

    @Override
    public int getItemCount() {
        return mIterator.getOldListSize();
    }

    @Override
    public int getItemViewType(int position) {
        return mIterator.getSpecialAtItem(position);
    }

    @SuppressLint("DefaultLocale")
    private void updateProgress(ViewHolderItem h) {
        if (h == null || h.item == null || h.item.mission instanceof FinishedMission) return;

        DownloadMission mission = (DownloadMission) h.item.mission;
        double done = mission.done;
        long length = mission.getLength();
        long now = System.currentTimeMillis();
        boolean hasError = mission.errCode != ERROR_NOTHING;

        // hide on error
        // show if current resource length is not fetched
        // show if length is unknown
        h.progress.setMarquee(mission.isRecovering() || !hasError && (!mission.isInitialized() || mission.unknownLength));

        double progress;
        if (mission.unknownLength) {
            progress = Double.NaN;
            h.progress.setProgress(0.0f);
        } else {
            progress = done / length;
        }

        if (hasError) {
            h.progress.setProgress(isNotFinite(progress) ? 1d : progress);
            h.status.setText(R.string.msg_error);
        } else if (isNotFinite(progress)) {
            h.status.setText(UNDEFINED_PROGRESS);
        } else {
            h.status.setText(String.format("%.2f%%", progress * 100));
            h.progress.setProgress(progress);
        }

        @StringRes int state;
        String sizeStr = Utility.formatBytes(length).concat("  ");

        if (mission.isPsFailed() || mission.errCode == ERROR_POSTPROCESSING_HOLD) {
            h.size.setText(sizeStr);
            return;
        } else if (!mission.running) {
            state = mission.enqueued ? R.string.queued : R.string.paused;
        } else if (mission.isPsRunning()) {
            state = R.string.post_processing;
        } else if (mission.isRecovering()) {
            state = R.string.recovering;
        } else {
            state = 0;
        }

        if (state != 0) {
            // update state without download speed
            h.size.setText(sizeStr.concat("(").concat(mContext.getString(state)).concat(")"));
            h.resetSpeedMeasure();
            return;
        }

        if (h.lastTimestamp < 0) {
            h.size.setText(sizeStr);
            h.lastTimestamp = now;
            h.lastDone = done;
            return;
        }

        long deltaTime = now - h.lastTimestamp;
        double deltaDone = done - h.lastDone;

        if (h.lastDone > done) {
            h.lastDone = done;
            h.size.setText(sizeStr);
            return;
        }

        if (deltaDone > 0 && deltaTime > 0) {
            float speed = (float) ((deltaDone * 1000d) / deltaTime);
            float averageSpeed = speed;

            if (h.lastSpeedIdx < 0) {
                Arrays.fill(h.lastSpeed, speed);
                h.lastSpeedIdx = 0;
            } else {
                for (int i = 0; i < h.lastSpeed.length; i++) {
                    averageSpeed += h.lastSpeed[i];
                }
                averageSpeed /= h.lastSpeed.length + 1.0f;
            }

            String speedStr = Utility.formatSpeed(averageSpeed);
            String etaStr;

            if (mission.unknownLength) {
                etaStr = "";
            } else {
                long eta = (long) Math.ceil((length - done) / averageSpeed);
                etaStr = Utility.formatBytes((long) done) + "/" + Utility.stringifySeconds(eta) + "  ";
            }

            h.size.setText(sizeStr.concat(etaStr).concat(speedStr));

            h.lastTimestamp = now;
            h.lastDone = done;
            h.lastSpeed[h.lastSpeedIdx++] = speed;

            if (h.lastSpeedIdx >= h.lastSpeed.length) h.lastSpeedIdx = 0;
        }
    }

    private void viewWithFileProvider(Mission mission) {
        if (checkInvalidFile(mission)) return;

        String mimeType = resolveMimeType(mission);

        if (BuildConfig.DEBUG)
            Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider");

        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(resolveShareableUri(mission), mimeType);
        intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
        intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION);
        ShareUtils.openIntentInApp(mContext, intent);
    }

    private void shareFile(Mission mission) {
        if (checkInvalidFile(mission)) return;

        final Intent shareIntent = new Intent(Intent.ACTION_SEND);
        shareIntent.setType(resolveMimeType(mission));
        shareIntent.putExtra(Intent.EXTRA_STREAM, resolveShareableUri(mission));
        shareIntent.addFlags(FLAG_GRANT_READ_URI_PERMISSION);

        final Intent intent = new Intent(Intent.ACTION_CHOOSER);
        intent.putExtra(Intent.EXTRA_INTENT, shareIntent);
        // unneeded to set a title to the chooser on Android P and higher because the system
        // ignores this title on these versions
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) {
            intent.putExtra(Intent.EXTRA_TITLE, mContext.getString(R.string.share_dialog_title));
        }
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION);

        mContext.startActivity(intent);
    }

    /**
     * Returns an Uri which can be shared to other applications.
     *
     * @see <a >
     * https://stackoverflow.com/questions/38200282/android-os-fileuriexposedexception-file-storage-emulated-0-test-txt-exposed</a>
     */
    private Uri resolveShareableUri(Mission mission) {
        if (mission.storage.isDirect()) {
            return FileProvider.getUriForFile(
                mContext,
                BuildConfig.APPLICATION_ID + ".provider",
                new File(URI.create(mission.storage.getUri().toString()))
            );
        } else {
            return mission.storage.getUri();
        }
    }

    private static String resolveMimeType(@NonNull Mission mission) {
        String mimeType;

        if (!mission.storage.isInvalid()) {
            mimeType = mission.storage.getType();
            if (mimeType != null && mimeType.length() > 0 && !mimeType.equals(StoredFileHelper.DEFAULT_MIME))
                return mimeType;
        }

        String ext = Utility.getFileExt(mission.storage.getName());
        if (ext == null) return DEFAULT_MIME_TYPE;

        mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1));

        return mimeType == null ? DEFAULT_MIME_TYPE : mimeType;
    }

    private boolean checkInvalidFile(@NonNull Mission mission) {
        if (mission.storage.existsAsFile()) return false;

        Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show();
        return true;
    }

    private ViewHolderItem getViewHolder(Object mission) {
        for (ViewHolderItem h : mPendingDownloadsItems) {
            if (h.item.mission == mission) return h;
        }
        return null;
    }

    @Override
    public boolean handleMessage(@NonNull Message msg) {
        if (mStartButton != null && mPauseButton != null) {
            checkMasterButtonsVisibility();
        }

        switch (msg.what) {
            case DownloadManagerService.MESSAGE_ERROR:
            case DownloadManagerService.MESSAGE_FINISHED:
            case DownloadManagerService.MESSAGE_DELETED:
            case DownloadManagerService.MESSAGE_PAUSED:
                break;
            default:
                return false;
        }

        ViewHolderItem h = getViewHolder(msg.obj);
        if (h == null) return false;

        switch (msg.what) {
            case DownloadManagerService.MESSAGE_FINISHED:
            case DownloadManagerService.MESSAGE_DELETED:
                // DownloadManager should mark the download as finished
                applyChanges();
                return true;
        }

        updateProgress(h);
        return true;
    }

    private void showError(@NonNull DownloadMission mission) {
        @StringRes int msg = R.string.general_error;
        String msgEx = null;

        switch (mission.errCode) {
            case 416:
                msg = R.string.error_http_unsupported_range;
                break;
            case 404:
                msg = R.string.error_http_not_found;
                break;
            case ERROR_NOTHING:
                return;// this never should happen
            case ERROR_FILE_CREATION:
                msg = R.string.error_file_creation;
                break;
            case ERROR_HTTP_NO_CONTENT:
                msg = R.string.error_http_no_content;
                break;
            case ERROR_PATH_CREATION:
                msg = R.string.error_path_creation;
                break;
            case ERROR_PERMISSION_DENIED:
                msg = R.string.permission_denied;
                break;
            case ERROR_SSL_EXCEPTION:
                msg = R.string.error_ssl_exception;
                break;
            case ERROR_UNKNOWN_HOST:
                msg = R.string.error_unknown_host;
                break;
            case ERROR_CONNECT_HOST:
                msg = R.string.error_connect_host;
                break;
            case ERROR_POSTPROCESSING_STOPPED:
                msg = R.string.error_postprocessing_stopped;
                break;
            case ERROR_POSTPROCESSING:
            case ERROR_POSTPROCESSING_HOLD:
                showError(mission, UserAction.DOWNLOAD_POSTPROCESSING, R.string.error_postprocessing_failed);
                return;
            case ERROR_INSUFFICIENT_STORAGE:
                msg = R.string.error_insufficient_storage_left;
                break;
            case ERROR_UNKNOWN_EXCEPTION:
                if (mission.errObject != null) {
                    showError(mission, UserAction.DOWNLOAD_FAILED, R.string.general_error);
                    return;
                } else {
                    msg = R.string.msg_error;
                    break;
                }
            case ERROR_PROGRESS_LOST:
                msg = R.string.error_progress_lost;
                break;
            case ERROR_TIMEOUT:
                msg = R.string.error_timeout;
                break;
            case ERROR_RESOURCE_GONE:
                msg = R.string.error_download_resource_gone;
                break;
            default:
                if (mission.errCode >= 100 && mission.err        
Sam C.

Ex-Atlassian. Looking to build scalable, secure systems...

7 个月

Aaron, this made my day!

要查看或添加评论,请登录

Aaron D'Amico的更多文章

  • 30 Days of Bugs: Day 10

    30 Days of Bugs: Day 10

    Welcome to "30 Days of Bugs," a campaign by 3Flatline, a startup that's all about making code safer. Our product, the…

  • 30 Days of Bugs: Day 9

    30 Days of Bugs: Day 9

    Welcome to "30 Days of Bugs," a campaign by 3Flatline, a startup that's all about making code safer. Our product, the…

  • 30 days of bugs: Day 8

    30 days of bugs: Day 8

    Welcome to "30 Days of Bugs," a campaign by 3Flatline, a startup that's all about making code safer. Our product, the…

    1 条评论
  • 30 days of bugs: Day 7

    30 days of bugs: Day 7

    Welcome to "30 Days of Bugs," a campaign by 3Flatline, a startup that's all about making code safer. Our product, the…

  • 30 days of bugs: Day 6

    30 days of bugs: Day 6

    Welcome to "30 Days of Bugs," a campaign by 3Flatline, a startup that's all about making code safer. Our product, the…

    3 条评论
  • 30 days of bugs: Day 5

    30 days of bugs: Day 5

    Welcome to "30 Days of Bugs," a campaign by 3Flatline, a startup that's all about making code safer. Our product, the…

  • 30 days of bugs: Day 4

    30 days of bugs: Day 4

    Welcome to "30 Days of Bugs," a campaign by 3Flatline, a startup that's all about making code safer. Our product, the…

    2 条评论
  • 30 days of bugs: Day 3

    30 days of bugs: Day 3

    Welcome to "30 Days of Bugs," a campaign by 3Flatline, a startup that's all about making code safer. Our product, the…

  • 30 days of bugs: Day 2

    30 days of bugs: Day 2

    Welcome to "30 Days of Bugs," a campaign by 3Flatline, a startup that's all about making code safer. Our product, the…

  • AI levels the playing field for new SaaS companies.

    AI levels the playing field for new SaaS companies.

    New AI based SaaS platforms will not disrupt market incumbents but AI does take away the incumbents' competitive…

社区洞察

其他会员也浏览了