Tauri v2: diseño de capabilities, permisos y scope con mínimo privilegio
En Tauri v2, dividimos el límite de confianza entre WebView, Rust y el sistema operativo API en capacidad·permiso·alcance y resumimos cómo minimizar los permisos de archivos, ventanas y contenido remoto.
La configuración de permisos es un mapa de límites de confianza, no una lista de funciones
Tauri Configurar permisos en v2 inicialmente parece agregar una cadena a un archivo JSON. Si desea abrir un archivo, ingrese fs, si desea manipular una ventana, ingrese window, si desea abrir un enlace externo, ingrese shell, y así sucesivamente. Cuanto más pequeña es la aplicación, más natural resulta elegir "pongamos los valores predeterminados y limpiémoslos más tarde".
Sin embargo, en la aplicación de escritorio, esta configuración no es sólo una opción. WebView Define hasta qué punto el código front-end que se ejecuta en su interior puede alcanzar la funcionalidad del sistema operativo. En otras palabras, capacidad·permiso·alcance no es una lista de verificación de capacidades sino más bien un mapa dibujado entre una interfaz de usuario de baja confianza y un sistema con mayores privilegios.
La estructura básica de Tauri es clara. Los núcleos y complementos Rust tienen acceso a los recursos del sistema, y WebView solicita sus funciones solo a través de la ruta expuesta IPC. Si su interfaz se ve afectada por XSS, dependencias débiles, HTML que no son de confianza o contenido externo incorrecto, para reducir sus pérdidas, debe limitar lo que es posible desde el principio.
Este artículo divide las tres palabras de Tauri v2 en un lenguaje práctico. La capacidad describe "qué ventana", el permiso describe "qué comando" y el alcance describe "qué rango de argumentos". Mientras que la Guía de introducción SvelteKit + Tauri v2 anterior trataba sobre cómo iniciar una aplicación, aquí diseñamos hasta dónde puede moverse la aplicación en la computadora del usuario. La diferencia del modelo de seguridad entre Tauri·Electron es artículo comparativo de aplicaciones de escritorio SvelteKit, y el límite entre dispositivos móviles y de escritorio es [Capacitor y puede verlos juntos en selección de Tauri criterios.
Separe la capacidad, el permiso y el alcance en una oración
La razón por la que estos tres conceptos son confusos es porque aparecen juntos en el archivo de configuración real. Pero si haces preguntas diferentes, los roles se vuelven más claros.
- capacidad: ¿Puede esta ventana o WebView tener este conjunto de capacidades?
- permiso: ¿Puede la interfaz llamar a este comando?
- alcance: Incluso si el comando está permitido, ¿es posible solo en qué ruta/URL/valor?
Tomemos como ejemplo una aplicación de edición de documentos. La ventana de edición principal debería leer y guardar los archivos Markdown en la carpeta de datos de la aplicación. Solo necesita cambiar el tema en la ventana de configuración y la ventana de inicio de sesión solo muestra la pantalla de autenticación externa. Tan pronto como otorga a estas tres ventanas los mismos permisos, el requisito del producto de que “la ventana de inicio de sesión no necesita escribir archivos” desaparece de la configuración de seguridad.
En este momento, la capacidad es una capa que permite que sólo la ventana principal tenga permisos de archivo. El permiso es una capa que permite comandos como read_text_file y write_text_file. alcance es una capa que trunca el alcance para que el comando funcione solo bajo $APPDATA/notes/*.md.
WebView(메인 편집 창)
└─ capability: main-editor
├─ permission: fs:allow-read-text-file
├─ permission: fs:allow-write-text-file
└─ scope: $APPDATA/notes/*.md
WebView(로그인 창)
└─ capability: auth-window
└─ permission: 필요한 창 제어만
Lo importante de esta estructura es que las capacidades están vinculadas a la etiqueta de la ventana, no al título de la ventana visible para el usuario. El título se puede traducir o cambiar, pero la etiqueta debe ser una identificación fija que identifique el límite de seguridad. Es mejor establecer etiquetas que revelen la función, como main, settings y auth, y no crear etiquetas con la entrada del usuario.
Por qué deberías empezar desde la unidad funcional más pequeña
En lugar de conceder permisos de forma amplia y luego reducirlos, es mucho mejor comprobar y añadir las funciones necesarias una por una. La razón es sencilla. Los permisos amplios aceleran el comportamiento normal, pero luego dificultan la respuesta a la pregunta "¿Por qué están aquí estos permisos?" A medida que la aplicación crece, la persona que creó el archivo la abandona y nadie está seguro de qué contiene una sola línea de default.
Primero, escriba las acciones del usuario como oraciones.
- Guarde las notas nuevas sólo en la ventana principal.
- La ventana de preferencias no maneja archivos locales.
- La importación ocurre solo cuando el usuario selecciona explícitamente un archivo.
- Abra enlaces externos en el navegador del sistema operativo, no en WebView dentro de la aplicación.
- Los valores secretos para la autenticación y el pago se verifican en el servidor en lugar de agregarse a la aplicación.
Luego agregue API y una ruta a cada acción. Las exigencias de “tratar con archivos” son demasiado grandes. Si su verdadera necesidad es "Guardar Markdown en una carpeta de Notas", entonces solo necesita un comando de escritura y una ubicación de carpeta de Notas, no todo el sistema de archivos. La calidad del diseño de permisos no proviene del número de líneas de configuración, sino de cuán específicos se traducen los requisitos del producto en preguntas de seguridad.
Las capacidades se dividen en cada ventana y la aplicación duplicada se realiza conscientemente.
Los archivos de capacidad Tauri v2 generalmente se colocan en src-tauri/capabilities. Las capacidades dentro de un directorio están activadas de forma predeterminada, por lo que dividir un archivo no lo aísla automáticamente. Si la misma ventana o WebView se incluye en más de una capacidad, las capacidades se combinan.
Esto es especialmente importante al crear un "archivo de permisos comunes". Si coloca todas sus ventanas en base.json y la ventana principal nuevamente en main-editor.json, la ventana principal recibirá los permisos de ambos archivos. Aunque los nombres están separados, los límites reales están combinados. Es más seguro colocar sólo las funciones de sólo lectura que son realmente necesarias para todas las ventanas en archivos comunes y dejar acceso nativo a capacidades específicas de cada función.
// src-tauri/capabilities/main-editor.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-editor",
"description": "메인 편집 창에서 노트 파일을 제한적으로 다루는 권한",
"windows": ["main"],
"permissions": [
"core:app:default",
"core:event:default",
"core:window:default",
"dialog:allow-open",
{
"identifier": "fs:allow-read-text-file",
"allow": [{ "path": "$APPDATA/notes/*.md" }]
},
{
"identifier": "fs:allow-write-text-file",
"allow": [{ "path": "$APPDATA/notes/*.md" }]
}
]
}
// src-tauri/capabilities/settings.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "settings-window",
"description": "환경설정 창의 최소 창 제어 권한",
"windows": ["settings"],
"permissions": [
"core:window:default",
"core:app:default"
]
}
El ejemplo restringe las lecturas y escrituras en la misma ruta. La razón por la que no puse fs:default aquí es para evitar ampliar los permisos sin comprobar qué contiene realmente el conjunto de permisos predeterminado. Tauri Si agrega un complemento mediante CLI, el permiso predeterminado del complemento puede reflejarse en la configuración, por lo que debe acostumbrarse a mirar el archivo de capacidades y el esquema creado nuevamente después de agregar un nuevo complemento.
Las variables de ruta como $APPDATA se interpretan como directorios de datos de aplicaciones según el sistema operativo. Es más portátil que escribir la carpeta de inicio del usuario directamente como una cadena y también es mejor para distinguir entre datos específicos de la aplicación y archivos personales. Sin embargo, glob es sólo una característica de conveniencia y no una política de producto. Si solo necesita "archivos Markdown administrados por la aplicación Notes" en lugar de "todos los archivos Markdown", es más seguro que la aplicación sea propietaria de la carpeta.
el permiso se lee como el alcance de influencia, no como el nombre del comando
El permiso permite que la interfaz llame a un comando IPC específico. Incluso dentro del mismo complemento, leer, escribir, eliminar y enumerar directorios tienen efectos diferentes. Por lo tanto, debes elegir sólo lo que necesitas.
Un error común con las aplicaciones de Archivos es la idea de que "tienes que abrir todo el archivo fs para poder leer y guardar". En realidad, el flujo para leer archivos para que el usuario los abra también es diferente del flujo para guardar borradores dentro de la aplicación. El primero requiere diálogo y permiso de lectura, y el segundo solo requiere permiso de escritura en los datos de la aplicación. Si no hay una función de eliminación, no se proporciona el permiso de eliminación. Si solo necesita exportar, revise por separado las funciones para sobrescribir archivos existentes y crear archivos nuevos.
import { open } from '@tauri-apps/plugin-dialog';
import { readTextFile } from '@tauri-apps/plugin-fs';
export async function selectMarkdownForImport(): Promise<string | null> {
const selected = await open({
directory: false,
multiple: false,
filters: [{ name: 'Markdown', extensions: ['md'] }]
});
if (typeof selected !== 'string') return null;
return readTextFile(selected);
}
Antes de agregar fs:default para decir que este código no funciona, verifique dos cosas: Primero, ¿la ruta devuelta por el cuadro de diálogo de selección de archivos está incluida en el alcance actual? En segundo lugar, ¿el producto realmente necesita leer archivos directamente desde la ubicación arbitraria del usuario? En el caso de una importación simple, es ventajoso tanto para el modelo de permisos como para la política de respaldo verificar el archivo seleccionado y luego copiarlo al directorio de datos de la aplicación y luego editarlo solo en la carpeta propiedad de la aplicación.
El mismo principio se aplica a los enlaces externos. shell:allow-open es conveniente para abrir documentos en un navegador, pero cuando la interfaz de usuario pasa cadenas arbitrarias como URL, crea problemas de UX que bordean el phishing y la ejecución involuntaria de protocolos. Es mejor limitar las URL a una lista de constantes o URL HTTPS verificadas por el servidor y no pasar los valores ingresados por el usuario directamente a las llamadas nativas.
alcance es la segunda pregunta después del permiso
El alcance limita los argumentos reales incluso después de activar el permiso. En los complementos del sistema de archivos, la ruta global es el ejemplo más familiar. Cuando permitir y denegar se usan juntos, denegar tiene prioridad, por lo que si realmente necesita un permiso amplio, puede especificar subrutas sensibles como denegar.
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "export-report",
"windows": ["main"],
"permissions": [
{
"identifier": "fs:allow-write-text-file",
"allow": [{ "path": "$APPDATA/reports/*.csv" }],
"deny": [{ "path": "$APPDATA/reports/private/*" }]
}
]
}
Sin embargo, este ejemplo no debe leerse como una prescripción de que “es seguro utilizar muchas opciones de permitir y denegar”. El mejor ámbito suele ser un directorio propietario pequeño y sencillo en lugar de una lista de excepciones compleja. En lugar de seguir agregando reports/private como excepción más adelante, es más fácil revisar si coloca el destino de exportación y los datos confidenciales en raíces diferentes desde el principio.
El alcance tampoco resuelve automáticamente todos los problemas. Un comando de complemento o aplicación debe interpretar y aplicar el alcance. En particular, no debe asumir que el comando Rust que usted mismo creó será bloqueado por el alcance porque recibió una cadena del frontend. Las entradas como nombres de archivos, URL e ID de registros también deben validarse nuevamente con reglas de dominio en el lado Rust.
Los comandos caseros Rust se tratan como un producto separado API
El comando Tauri es IPC API entre el frontend y Rust. Solo porque se llama solo en la interfaz de usuario, sería difícil tratarlo como una función interna. Los tipos de argumentos, la longitud máxima, las reglas de nombres de archivos, las comprobaciones de permisos y los mensajes de error deben diseñarse como públicos API.
Tomando como ejemplo el guardado de notas, es mejor que Front envíe solo el nombre y el contenido de la nota y que Rust cree la ruta de destino en la carpeta de datos de la aplicación, en lugar de que Front envíe una ruta aleatoria y que Rust la use tal como está.
use std::fs;
use tauri::Manager;
#[tauri::command]
fn save_note(
app: tauri::AppHandle,
note_id: String,
markdown: String,
) -> Result<(), String> {
if note_id.is_empty()
|| note_id.len() > 80
|| !note_id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
return Err("유효하지 않은 노트 식별자입니다.".into());
}
if markdown.len() > 2_000_000 {
return Err("노트 크기가 허용 범위를 넘었습니다.".into());
}
let notes_dir = app
.path()
.app_data_dir()
.map_err(|error| error.to_string())?
.join("notes");
fs::create_dir_all(¬es_dir).map_err(|error| error.to_string())?;
fs::write(notes_dir.join(format!("{note_id}.md")), markdown)
.map_err(|error| error.to_string())
}
Dado que este comando no recibe una ruta como argumento, reduce la posibilidad de interpretar cadenas de búsqueda de rutas como ../ como rutas de archivos. Por supuesto, este ejemplo es sólo un patrón. Si su editor tiene archivos grandes, archivos adjuntos binarios o requiere que el usuario los guarde en la ubicación original, surgen diferentes necesidades. Incluso entonces, el núcleo es el mismo. Para mayor comodidad, no pase los argumentos recibidos de la recepción directamente al sistema operativo API, sino limítelos a las reglas del producto en el lado Rust.
Si desea limitar los comandos propios de la aplicación a las capacidades, también puede especificar el manifiesto del comando en la etapa de compilación. Los comandos de aplicaciones registradas están disponibles para todas las ventanas y WebView de forma predeterminada, por lo que vale la pena considerarlos para comandos confidenciales.
// src-tauri/build.rs
fn main() {
tauri_build::try_build(
tauri_build::Attributes::new()
.app_manifest(
tauri_build::AppManifest::new().commands(&["save_note"])
)
)
.unwrap();
}
Y declarar el permiso para la aplicación a src-tauri/permissions.
# src-tauri/permissions/notes.toml
[[permission]]
identifier = "save-note"
description = "앱 데이터 디렉터리에 검증된 노트를 저장한다."
commands.allow = ["save_note"]
// src-tauri/capabilities/main-editor.json의 permissions 일부
[
"save-note"
]
Aquí, no es necesario que el nombre del permiso y el nombre del comando sean la misma cadena. permiso es el nombre de la política de permisos revisable por humanos y comando es la función IPC real. Es mejor darle un nombre que diga "qué ventana puede realizar qué comportamiento de producto".
Los permisos locales no se otorgan de forma predeterminada al contenido remoto.
Existe un nivel diferente de confianza entre el código de la aplicación dentro de un paquete y el contenido proveniente de Internet. El acceso remoto a API para Tauri no está abierto de forma predeterminada y debe configurar la capacidad por separado para exponer comandos nativos a URL remotas. En lugar de simplemente activarlo por conveniencia, esta es una característica que pregunta si la página remota realmente necesita llamar a la función nativa.
Puede mostrar ayuda basada en web, pantallas de OAuth y paneles remotos dentro de la aplicación WebView. Sin embargo, en la mayoría de los casos, la pantalla no requiere permisos como acceso a archivos, escritura en el portapapeles, base de datos local o creación de ventanas. Si su aplicación requiere contenido remoto, elija primero una de las siguientes opciones:
- La pantalla remota se abre en el navegador del sistema y está separada de los permisos de la aplicación.
- Muestre la pantalla remota en una etiqueta separada, WebView, y no adjunte capacidades locales.
- Exponga solo uno o dos comandos realmente necesarios al patrón de URL HTTPS correcto y pruebe por separado las diferencias por iframe, redirección y plataforma.
En particular, la documentación también establece la limitación de que Linux y Android pueden no poder distinguir entre solicitudes de iframe y solicitudes para la ventana misma. Es por eso que no debe terminar con la conclusión: "El dominio está en la lista permitida, por lo que es seguro". Las funciones que contienen URL remotas deben revisarse junto con su diseño funcional, CSP, redireccionamiento de autenticación, comportamiento de WebView y diseño de permisos.
Si está considerando dispositivos móviles, primero separe las plataformas
Tauri v2 está dirigido tanto a dispositivos móviles como a ordenadores de escritorio. Sin embargo, aplicar las mismas capacidades a todas las plataformas no significa reutilizar el código. Las capacidades de los complementos que existen sólo en el escritorio, como los accesos directos globales, y las funciones cercanas a los dispositivos móviles, como NFC y la autenticación biométrica, deben dividirse por plataforma.
// src-tauri/capabilities/desktop-shortcuts.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-shortcuts",
"windows": ["main"],
"platforms": ["linux", "macOS", "windows"],
"permissions": ["global-shortcut:allow-register"]
}
Esta separación no es sólo una forma de evitar errores de compilación. La misma característica del producto puede ser un método abreviado de teclado en el escritorio o un comportamiento del sistema operativo completamente diferente, como la autenticación biométrica o las hojas compartidas del sistema en el móvil. Al comparar las opciones para ejecutar un WebView móvil, incluido Capacitor, la pregunta más importante es en qué medida se deben diseñar los permisos y las expectativas del usuario de manera diferente a la cantidad de código de interfaz de usuario que se comparte.
El orden en que se resuelven los errores de permisos determina la calidad de los permisos.
Los errores más comunes que se encuentran en la práctica son simples. Se realizó la convocatoria, pero el rechazo indicó que no estaba permitida. En este momento, si sigue los pasos a continuación, puede limitar la causa y evitar ampliar la autoridad innecesariamente.
- Consulte el esquema de creación y la documentación oficial para ver qué identificador de permiso requiere realmente el llamado API.
- Verifique que la etiqueta de la ventana de llamada esté exactamente en
windowsde la capacidad correspondiente. - Compruebe si se aplican varias capacidades a la misma ventana y si los permisos no se combinan inesperadamente.
- Intente separar el permiso y el alcance de los argumentos de API. Compruebe si el comando está bloqueado o si la ruta/URL está fuera de alcance.
- Verifique tanto Rust como la interfaz para asegurarse de que la ruta del archivo, la URL y los argumentos del comando cumplan las reglas del producto.
- Verifique no solo en el modo de desarrollo sino también en el paquete real. En particular, la URL remota y WebView específico de la plataforma deberían verse en los resultados del paquete.
Este proceso puede parecer frustrante, pero también le permite dividir las solicitudes de su aplicación en oraciones más pequeñas cada vez que encuentre un error de permiso. La configuración y las pruebas se vuelven mucho más claras si la pregunta se cambia de "¿Debo abrir el sistema de archivos" a "¿Debo guardar un borrador de los datos de la aplicación solo en la ventana principal?"
Lista de verificación de privilegios mínimos previa a la implementación
Antes del lanzamiento, tómese el tiempo para leer los archivos de capacidades como si fueran temas de revisión de código. Si puede responder "sí" a las siguientes preguntas, es un buen punto de partida.
- ¿Están todas las capacidades
windowsasociadas con una etiqueta fija en lugar de una función visible para el usuario? - ¿Puede explicar el motivo para adjuntar múltiples capacidades a una ventana y los permisos que se combinan?
- ¿Ha verificado el rango de inclusión para cada línea donde agregó el conjunto de permisos
default? - ¿Leer, escribir, eliminar, ejecutar y abrir una URL externa se trata como impactos diferentes?
- ¿El alcance del archivo está limitado al directorio propiedad de la aplicación o a una ruta absolutamente necesaria seleccionada por el usuario en lugar de a toda la casa?
- ¿Siguen aumentando las excepciones de denegación de alcance? Entonces, ¿es posible cambiar la estructura del directorio a algo más pequeño?
- ¿El comando Rust no verifica la ruta, la URL, el identificador y el tamaño, y pasa la entrada de la interfaz de usuario tal como está a la llamada del sistema?
- ¿Has dejado abierto el API nativo local en una página remota o iframe?
- ¿Ha separado el permiso solo para escritorio y el permiso solo para dispositivos móviles en
platforms? - ¿Ha probado los permisos por ventana tanto en el modo de desarrollo como en el paquete de lanzamiento?
La seguridad no está completa sólo con los archivos de permisos
Aunque la capacidad·permiso·alcance es poderosa, mágicamente no hace que una aplicación completa sea segura. Rust Si el núcleo confía en la entrada incorrecta, el alcance es demasiado amplio o existen vulnerabilidades propias y problemas de la cadena de suministro, la configuración por sí sola no puede resolverlos. Por lo tanto, es más exacto ver este modelo no como “una característica de seguridad”, sino como una estructura básica que reduce el alcance del impacto en caso de un incidente.
Una buena aplicación Tauri no es una aplicación con muchos permisos, sino una aplicación donde los motivos de los permisos son claros en comparación con las funciones del usuario. La ventana de edición principal puede guardar notas, pero la ventana de inicio de sesión no. Incluso si los archivos se pueden leer, solo se pueden hacer en carpetas propiedad de la aplicación, y Rust verifica nuevamente las solicitudes de WebView. Si tienes esto en cuenta, la frontera entre la interfaz de usuario y el sistema operativo se vuelve mucho más manejable.
El diseño de privilegios mínimos de Tauri v2 no es un proceso que ralentice el desarrollo. Es un diseño de producto que deja claro “qué ventana, qué comando y para qué datos” cada vez que se agrega una característica. Las configuraciones que pueden responder esa pregunta se pueden mantener a lo largo del tiempo, y las configuraciones que no pueden responder esa pregunta algún día se convertirán en una deuda llamada default.