Saltar al contenido

Seguridad (CSP)

Protege a tus clientes garantizando que tus cotizaciones solo se puedan abrir donde tú lo autorizas.

La Amenaza de Clickjacking

Si cualquier página web en Internet pudiera simplemente crear un <iframe> apuntando a las cotizaciones privadas de tus clientes, un atacante podría construir una página falsa superpuesta (UI Redressing o Clickjacking) y engañar a un cliente para que haga clic en un botón de pago falso que en realidad hace clic en el botón de aceptar dentro de tu cotización invisible.

Por ello, la seguridad de Cord Elements es estricta desde el primer día.

Frame Ancestors y la Allowlist

Cord mitiga esta vulnerabilidad mediante encabezados HTTP nativos conocidos como Política de Seguridad de Contenido (CSP). Específicamente, usamos la directiva frame-ancestors.

Para que el snippet de instalación de Elements funcione en tu sitio web corporativo, primero debes registrar tus orígenes en el panel de control.

  1. Dirígete a Ajustes › Cotizador embebible, en la sección Desarrolladores del índice (aparece con el Modo desarrollador activo en Tu cuenta › Preferencias).
  2. Busca el paso Dominios autorizados.
  3. Añade los dominios separados por saltos de línea.

Ejemplos válidos

El sistema de dominios permite orígenes estrictos o el uso de comodines (wildcards):

  • tu-empresa.com (Autoriza solo la raíz).
  • *.tu-empresa.com (Autoriza la raíz y cualquier subdominio, como app.tu-empresa.com).
  • https://portal.tu-empresa.com (Altamente específico).

Sin dominios: solo lectura

Si la lista de dominios está vacía, cualquier sitio puede embeber la cotización, pero solo para mostrarla. Aprobar, firmar y pagar no se ofrecen dentro del iframe: en su lugar aparece un botón que abre la propuesta en una pestaña nueva, en el dominio de Cord, donde ningún sitio ajeno puede superponer nada. Los eventos salen sin detail (sin nombre de quien firma ni URL de pago).

Con al menos un dominio registrado, frame-ancestors restringe a todos los ancestros del iframe y el cotizador es interactivo.

Los eventos solo llegan a tu origen

El SDK adjunta el parámetro parentOrigin (el window.location.origin de tu página) a la URL del iframe. Si coincide con tu lista, window.postMessage dirige los eventos únicamente a ese origen. Sin coincidencia, el iframe sigue protegido por frame-ancestors: solo una página de tus dominios puede estar en la cadena de frames.

El pago nunca corre dentro del iframe

La página de pago no se deja enmarcar en ningún caso. Al pagar desde un embed, Cord la abre en una ventana propia y el evento cord:pay trae su URL absoluta. Así los datos de la tarjeta nunca pasan por un documento que tu sitio, o uno ajeno, pueda cubrir.

Llaves

  • Publishable Key (pk_live_): solo funciona con dominios permitidos configurados. Sin ellos, la API responde origin_allowlist_required. Las pk_test_ funcionan desde cualquier origen, incluido localhost.
  • Secret Key (sk_): jamás en un navegador. Si una llega desde uno, la API responde secret_key_in_browser y debes revocarla: ya quedó expuesta en el código de la página. @flouviahq/node también se niega a correr en un navegador.

CSP de tu página

Lo anterior protege a tus clientes de páginas ajenas. Si tu página tiene su propia Content Security Policy, esto es lo mínimo que necesita cada superficie de Elements. Con baseUrl (staging o self-host), cambia https://cordhq.app por ese origen.

Superficie frame-src connect-src style-src font-src
Documento con embed.js, mountCotizador() o <CordCotizador> (React y Vue) https://cordhq.app — 'nonce-…' —
<cord-quote> y <cord-invoice> https://cordhq.app — — —
<CordBuilder> con llave publicable — https://cordhq.app 'nonce-…' —
<CordBuilder> con proxy — el origen de proxyUrl ('self' en tu dominio) 'nonce-…' —
Fiscal Element (<cord-fiscal-form>, <CordFiscalForm>) — — — —
appearance.fonts en <CordProvider> — — https://fonts.googleapis.com o https://fonts.bunny.net https://fonts.gstatic.com o https://fonts.bunny.net
  • El documento vive en un iframe de Cord: lo que carga adentro lo rige la CSP de Cord, no la tuya. Tu página solo necesita enmarcarlo (frame-src) y, con embed.js, cargar el script (script-src https://cordhq.app o el nonce en su <script>). Los eventos llegan por postMessage, que la CSP no restringe, y el pago se abre en una ventana de Cord.
  • Estilos. El skeleton del documento y la hoja base del Builder son un <style> que el SDK agrega a tu <head>: con el nonce no necesitas 'unsafe-inline'. <cord-quote>, <cord-invoice>, <cord-fiscal-form> y la barra de depuración ponen sus estilos en su Shadow DOM como hojas construidas, que la CSP no bloquea; en un navegador sin ellas (Safari anterior a 16.4) usan un <style> con el mismo nonce. Con baseTheme: 'none' el Builder no agrega su hoja.
  • Variables de la appearance. El Builder las pone en el atributo style de su raíz. Dibujado en el navegador no necesita nada más: React las aplica sin pasar por la CSP. Renderizado en el servidor (Next.js, Remix), viajan en el HTML y una CSP con nonce las bloquea: agrega style-src-attr 'unsafe-inline' o móntalo solo en el navegador.
  • Red. Con llave publicable, el Builder llama a https://cordhq.app/api/v1 (configuración, catálogo, cotización y lectura de pedidos con IA); con proxy, solo a tu proxyUrl. useQuoteBuilder() y createCordClient() sin <CordBuilder> solo necesitan ese connect-src. El Fiscal Element valida en el navegador y no hace peticiones, con proxy o sin él: sus datos viajan con la cotización del Builder.
  • Trusted Types. Elements no escribe HTML con innerHTML ni evalúa código, así que funciona con require-trusted-types-for 'script'.
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-NONCE' https://cordhq.app; frame-src https://cordhq.app; connect-src 'self' https://cordhq.app; style-src 'self' 'nonce-NONCE'

El nonce

Es el mismo valor de 'nonce-…' en tu cabecera, distinto en cada respuesta. Elements lo pone en cada <style> y <link> que agrega a tu página:

  • configureCord({ nonce }): para mountCotizador(), los Web Components y el Fiscal Element. Llámalo antes de montar.
  • <CordProvider nonce={nonce}>: React. En Next.js, lee el nonce de la petición en un componente de servidor y pásalo al Provider.
  • <CordCotizador nonce={nonce}>: el visor de @flouviahq/elements/react/viewer y el de Vue, que no usan Provider.
  • embed.js: pon el nonce en su <script> y el loader lo usa para sus estilos.
<script src="https://cordhq.app/embed.js" nonce="NONCE" async></script>
<div data-cord-token="a1b2c3d4e5f60718"></div>
import { headers } from 'next/headers';
import { CordProvider } from '@flouviahq/elements/react';

export default async function Layout({ children }: { children: React.ReactNode }) {
  const nonce = (await headers()).get('x-nonce') ?? undefined;
  return <CordProvider publishableKey="pk_test_…" nonce={nonce}>{children}</CordProvider>;
}

Sin fuentes de Google

Solo <CordProvider> carga fuentes en tu página: por cada appearance.fonts, un <link> a Google Fonts o Bunny Fonts, con tu nonce y sin referrer. Si tu CSP no permite esos orígenes o prefieres servir la fuente tú, apágalo:

configureCord({ loadFonts: false });
// o <CordProvider loadFonts={false}>

La familia (variables.fontFamily o la de la primera fuente) se sigue aplicando y la fuente la cargas tú. El iframe del documento carga sus fuentes bajo la CSP de Cord; si no quieres que pida nada a Google, no pases fonts.