Is Backpack safe?

Medium risk

Backpack is medium risk. Backpack's wallet creation makes a WebAuthn passkey, calls 2231dcc2.sslip.io/v0/derive-public-key, and builds wallet init records from the returned keys. The POST carries an embedded Bearer JWT. Testing couldn't reach this gated branch.

Backpackv0.10.215Chrome Web Store
45Risk

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

Passkey Wallet Setup Posts Derived Public Keys

Backpack's wallet creation makes a WebAuthn passkey, calls 2231dcc2.sslip.io/v0/derive-public-key, and builds wallet init records from the returned keys.

The POST carries an embedded Bearer JWT.

Testing couldn't reach this gated branch.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You start Backpack's passkey wallet creation flow.

The branch is tied to browser WebAuthn passkey creation, not the standard mnemonic wallet flow.

The extension did this

The extension derives public keys and prepares wallet records from the passkey result.

The service worker posts derivation inputs to 2231dcc2.sslip.io with an embedded Bearer token.

Captured request
POSThttps://2231dcc2.sslip.io/v0/derive-public-key

The WebAuthn-gated passkey branch was not executed during testing, so no request body or response body was recorded.

Headers
Content-Type
application/json
Authorization
Bearer <redacted embedded JWT>
Fields assembled by the passkey wallet branch
  • Derived public key
    9xQeWvG816bUx9EPfN8uR5WzQn3s9Y7vZyK9G6aYb7H8

    Lets the new passkey wallet link to a public wallet address per chain. Illustrative; no live request body was recorded.

  • Derivation domain
    ed25519

    Shows whether the requested derivation is for an Ed25519 or Secp256k1 wallet family.

  • Passkey credential ID
    V2ViQXV0aG4tY3JlZGVudGlhbC1pZC0yMDI2

    Ties the wallet init record to the browser passkey created during setup. Illustrative; no live request body was recorded.

  • Embedded JWT claims
    {"v":1,"key_type":"distribution_channel","partner_id":"backpack","iat":1773125535,"exp":1804661535}

    Identifies the request as Backpack distribution-channel traffic and gives the token's validity window.

The code that does this

The request helper posts JSON to the configured base URL

Readable version
class OneClickClient {  constructor(options) {    const baseUrl = options.baseUrl.trim();    this.baseUrl = baseUrl.startsWith("http://") || baseUrl.startsWith("https://") ? baseUrl : `https://${baseUrl}`;    this.authToken = options.authToken;  }  async derivePublicKey(predecessor, path, domainId) {    const response = await this.post("/v0/derive-public-key", { path, domainId, predecessor });    if (!response.ok) {      const text = await response.text().catch(() => "");      throw new Error(`OneClickClient.derivePublicKey failed: HTTP ${response.status} — ${text}`);    }    const publicKey = await response.text();    const prefix = `${domainId}:`;    if (!publicKey.startsWith(prefix)) {      throw new Error(`OneClickClient.derivePublicKey: unexpected response format — expected "${prefix}...", got "${publicKey.slice(0,64)}"`);    }    return publicKey;  }  async post(path, body) {    const headers = { "Content-Type": "application/json" };    if (this.authToken) headers.Authorization = `Bearer ${this.authToken}`;    return fetch(`${this.baseUrl}${path}`, {      method: "POST",      headers,      body: JSON.stringify(body)    });  }}
The code that does this

The passkey branch configures sslip.io and builds wallet initialization data

Readable version
async function createPasskeyWalletInits(networkIds, options = {}) {  const supportedNetworks = networkIds.flatMap((networkId) => {    const config = q6[networkId];    return config ? [{ networkId, config }] : [];  });  if (supportedNetworks.length === 0) {    throw new Error("No supported networks selected for passkey wallet");  }  const passkeyWallet = await c9.createWallet({ displayName: options.displayName });  const { address, credentialId } = passkeyWallet;  const needsEd25519 = supportedNetworks.some((entry) => entry.config.domain === z6.Ed25519);  const needsSecp256k1 = supportedNetworks.some((entry) => entry.config.domain === z6.Secp256k1);  const client = new j6({    baseUrl: "https://2231dcc2.sslip.io",    authToken: "<redacted embedded JWT>"  });  const [ed25519PublicKey, secp256k1PublicKey] = await Promise.all([    needsEd25519 ? client.derivePublicKey(address, "", z6.Ed25519) : Promise.resolve(""),    needsSecp256k1 ? client.derivePublicKey(address, "", z6.Secp256k1) : Promise.resolve("")  ]);  return {    walletInits: supportedNetworks.map(({ networkId, config }) => {      const derivedPublicKey = config.domain === z6.Ed25519 ? ed25519PublicKey : secp256k1PublicKey;      return {        type: By.PASSKEY,        blockchain: wy(networkId),        blockchainId: networkId,        walletType: config.walletType,        publicKeyInfo: { publicKey: config.deriveAddress(derivedPublicKey) },        credentialId      };    })  };}
The code that does this

The passkey setup is gated by browser WebAuthn creation

Readable version
async createPasskey(options) {  if (!navigator.credentials) throw new Error("WebAuthn not supported in this browser");  const challenge = options.challenge ?? crypto.getRandomValues(new Uint8Array(32));  const userId = crypto.getRandomValues(new Uint8Array(32));  const credential = await navigator.credentials.create({    publicKey: {      challenge: challenge.buffer,      rp: { id: e9, name: "Backpack" },      user: { id: userId.buffer, name: options.displayName, displayName: options.displayName },      pubKeyCredParams: [        { alg: -7, type: "public-key" },        { alg: -8, type: "public-key" }      ],      authenticatorSelection: {        authenticatorAttachment: "platform",        userVerification: "required",        residentKey: "preferred"      },      timeout: 60000,      extensions: { prf: { eval: { first: X6.buffer } } }    }  });  if (!credential) throw new Error("Failed to create passkey");  const extensionResults = credential.getClientExtensionResults();  const prfEnabled = extensionResults.prf?.enabled ?? false;  if (!prfEnabled) {    throw new Error("PRF extension not supported on this device. Requires iOS 18+, Chrome 108+, or Edge 108+.");  }  const prfOutput = extensionResults.prf?.results?.first;  return {    id: credential.id,    publicKey: new Uint8Array(credential.response.getPublicKey() ?? []),    prfEnabled,    prfOutput: prfOutput ? new Uint8Array(prfOutput) : undefined  };}
External destination used by this flow
    • 2231dcc2.sslip.io

    Receives passkey public-key derivation POSTs from the service worker during passkey wallet setup.

Updated 30 September 2026aflkmfhebedbjioipglgcbcmnbpgliof