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.
- Dirígete a Ajustes › Cotizador embebible, en la sección Desarrolladores del índice (aparece con el Modo desarrollador activo en Tu cuenta › Preferencias).
- Busca el paso Dominios autorizados.
- 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, comoapp.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 respondeorigin_allowlist_required. Laspk_test_funcionan desde cualquier origen, incluidolocalhost. - Secret Key (
sk_): jamás en un navegador. Si una llega desde uno, la API respondesecret_key_in_browsery debes revocarla: ya quedó expuesta en el código de la página.@flouviahq/nodetambié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, conembed.js, cargar el script (script-src https://cordhq.appo el nonce en su<script>). Los eventos llegan porpostMessage, 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. ConbaseTheme: 'none'el Builder no agrega su hoja. - Variables de la appearance. El Builder las pone en el atributo
stylede 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: agregastyle-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 tuproxyUrl.useQuoteBuilder()ycreateCordClient()sin<CordBuilder>solo necesitan eseconnect-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
innerHTMLni evalúa código, así que funciona conrequire-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 }): paramountCotizador(), 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/viewery 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.