Skip to content

LastPass import silently drops a large fraction of attachments during upload_v3_attachments — no warning, no error, no log #2208

Description

@jaseonline

Environment

  • Commander version: 18.0.9 (reproduced; latest at time of writing is 18.0.11, same code path present on master)
  • OS: Windows 11 (PowerShell 7)
  • Import command used:
    keeper import --format=lastpass <email> --file-cache=<dir> --folder=NEW_IMPORT --update --show-skipped
    
  • Source vault: LastPass export with 58 file attachments across a set of Secure Notes / Logins

Summary

When importing from LastPass, attachment download and decryption both succeed for all 58 attachments (confirmed — see evidence below), but only a consistent subset (~24 of 58) actually get uploaded to Keeper via upload_v3_attachments(). The remaining ~34 are dropped with no console output, no logging.warning, no exception, and no entry in --debug file logging — the import reports full success (e.g. 957 records imported successfully) and the "Uploading v3 attachments:" section simply never mentions the missing files.

This is fully reproducible: across four independent runs (a plain import, two --update re-imports, and a --debug run), the same ~24 files upload every time and the same ~34 never do — regardless of whether --update is used, ruling out the record-deduplication path as the cause.

Steps to reproduce

  1. Have a LastPass account with a mix of attachment types/sizes across many records (in our case: certs/keys/PFX files plus many small MFA-backup .png screenshots).
  2. Run import --format=lastpass <email> --file-cache=<dir>.
  3. Compare the count of N. Downloading <file> ... Done lines (58) against the filenames actually listed under Uploading v3 attachments: (24).
  4. Re-run with --update against the same target folder — the same ~24 files are the only ones ever uploaded, every time.

Expected behavior

All 58 downloaded/decrypted attachments are uploaded, or, if some cannot be uploaded, Commander logs a clear reason per file (as it already does in several other code paths — see below).

Actual behavior

~34 of 58 attachments vanish between the download phase and the vault/files_add request with zero diagnostic output.

Diagnostic evidence

We spent considerable effort ruling out every logged failure path in keepercommander/importer/lastpass/vault.py, parser.py, and keepercommander/importer/imp_exp.py:

  • decrypted_file_sizes.json (written by Vault.process_attachments() after successfully decrypting each attachment purely to measure its size) shows valid, nonzero sizes for every attachment that was ever cached — ruling out decrypt corruption/failure (vault.py's skip_bad_attachments path, which does log a warning, never fired).
  • A full session captured with debug --file <path> (root logger at DEBUG, capturing every logging.* call for the entire session, including the vault/files_add REST request/response) contained exactly 2 WARNING-level lines in 2.6MB of output, both the unrelated "Previous temporary directories from interrupted imports detected" cleanup notice. Zero occurrences of:
    • Attachment {name} is corrupted and failed to decrypt (vault.py)
    • Attachment {name} failed to download (vault.py)
    • Upload of {name} failed: Parent record ... is missing (imp_exp.py, upload_v3_attachments)
    • Shared folder decryption error (parser.py, parse_SHAR)
  • The vault/files_add REST request itself (captured via --debug) consistently contains ~23–24 file entries — matching the same subset every run.
  • Re-running with --update (which triggers the dedup/reconciliation path in imp_exp.py comparing title+size against a matched existing record's current fileRef list) produces an identical result to the very first plain import, which had no pre-existing duplicate records to match against at all — ruling out the reconciliation logic (elif r.uid in external_lookup: block) as the root cause, since it could only apply on subsequent runs.

This points at one of the two genuinely silent continue statements in upload_v3_attachments() (keepercommander/importer/imp_exp.py):

atta.prepare()
if isinstance(atta.size, int):
    if atta.size == 0:
        continue
    if atta.size > 100 * 2 ** 20:
        logging.warning(...)
        continue
else:
    continue

Neither branch logs anything. Given the decrypted sizes we can observe are all valid and nonzero, either:

  1. atta.size is not consistently populated by the time this runs for every attachment object (e.g. a given ImportAttachment instance reaching this function is not the same object instance that had .size set during the earlier Vault.process_attachments() pass), or
  2. There's a batching/ordering issue elsewhere between record parsing and this function that causes a subset of attachment objects to arrive here without their size having been computed.

We were not able to pin down which of these two exactly, because both are silent by design — reproducing this reliably but being unable to get a definitive answer even with full DEBUG-level logging enabled is itself part of the bug report.

Suggested fix

Add a logging.debug or logging.warning call in both silent branches of upload_v3_attachments(), e.g.:

if isinstance(atta.size, int):
    if atta.size == 0:
        logging.warning(f'Skipping {atta.name}: computed size is 0')
        continue
    ...
else:
    logging.warning(f'Skipping {atta.name}: size not set (type={type(atta.size)})')
    continue

This alone would have made root-causing this a five-minute job instead of the multi-hour investigation it took us, and would surface this class of bug to any future user experiencing the same silent data loss.

Workaround

We wrote a small standalone script using Commander's own LastPassImporter/Vault/attachment classes to decrypt every attachment directly to a local folder, bypassing the Keeper upload path entirely:

from keepercommander.importer.lastpass.lastpass import LastPassImporter

importer = LastPassImporter()
records = importer.do_import(username, tmpdir=file_cache_dir)
for item in records:
    for atta in getattr(item, 'attachments', None) or []:
        with atta.open() as src, open(out_path, 'wb') as dst:
            shutil.copyfileobj(src, dst)

This confirms all 58 attachments decrypt correctly outside of the Keeper upload path, reinforcing that the loss happens specifically within upload_v3_attachments() or just before it, not during LastPass-side decryption.

Happy to share the full reproduction log (with sensitive values redacted) if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions