fix(security): address medium findings for ACL, limits, and UX

Tighten approve-endpoint to active shares with TTL sessions, reject non-file
FDs, lower default ticket/approval/size caps, show endpoint IDs and Public-mode
warnings, harden Android receive path checks, and run cargo-audit in CI.
This commit is contained in:
2026-07-13 18:54:28 +02:00
parent e7fb0331b5
commit d0c8d8774a
18 changed files with 184 additions and 14 deletions

View File

@@ -22,6 +22,7 @@
<string name="send_access_approval_description">You approve or refuse every new receiver.</string>
<string name="send_access_anyone">Anyone with this transfer</string>
<string name="send_access_anyone_description">No approval is required. Only use this for files you are comfortable sharing.</string>
<string name="send_access_anyone_warning">Anyone who has the ticket can download until you stop the share. Do not use this for private or sensitive files.</string>
<string name="send_file_size_unknown">Size unavailable</string>
<string name="send_transfer_created">Transfer created.</string>
<string name="send_transfer_details_title">Transfer details</string>
@@ -173,6 +174,7 @@
<string name="value_on">On</string>
<string name="value_off">Off</string>
<string name="approval_connection_request">Connection request</string>
<string name="approval_endpoint_id">Endpoint ID: %1$s</string>
<string name="approval_pending_count">%1$d requests are waiting</string>
<string name="core_status_ready">Ready</string>
<string name="event_log_title">Event log</string>

View File

@@ -27,6 +27,8 @@ data class PendingApproval(
val transferName: String,
val receiverName: String?,
val receiverDeviceName: String?,
/** Cryptographic peer identity from the Iroh connection — not display-name spoofable. */
val remoteEndpointId: String,
val requestedAt: Long,
)
@@ -162,6 +164,7 @@ private fun ReceiverRequestModel.toPending(): PendingApproval = PendingApproval(
transferName = transferName,
receiverName = receiverName,
receiverDeviceName = receiverDeviceName,
remoteEndpointId = remoteEndpointId,
requestedAt = requestedAt,
)

View File

@@ -31,6 +31,7 @@ import com.vnidrop.app.ui.theme.LocalVniDropColors
import org.jetbrains.compose.resources.stringResource
import vnidrop.shared.generated.resources.Res
import vnidrop.shared.generated.resources.approval_connection_request
import vnidrop.shared.generated.resources.approval_endpoint_id
import vnidrop.shared.generated.resources.approval_pending_count
import vnidrop.shared.generated.resources.button_approve
import vnidrop.shared.generated.resources.button_refuse
@@ -78,6 +79,12 @@ fun ApprovalModalHost(
style = MaterialTheme.typography.bodyLarge,
color = colors.foregroundLight,
)
// Trusted identity is the endpoint id; display names are peer-provided.
Text(
stringResource(Res.string.approval_endpoint_id, request.remoteEndpointId),
style = MaterialTheme.typography.bodySmall,
color = colors.foregroundLighter,
)
if (state.pending.size > 1) {
Text(
stringResource(Res.string.approval_pending_count, state.pending.size),

View File

@@ -51,6 +51,7 @@ import vnidrop.shared.generated.resources.field_sender_name
import vnidrop.shared.generated.resources.field_transfer_name
import vnidrop.shared.generated.resources.send_access_anyone
import vnidrop.shared.generated.resources.send_access_anyone_description
import vnidrop.shared.generated.resources.send_access_anyone_warning
import vnidrop.shared.generated.resources.send_access_approval
import vnidrop.shared.generated.resources.send_access_approval_description
import vnidrop.shared.generated.resources.send_access_title
@@ -166,6 +167,13 @@ private fun ReviewFileStep(
selected = state.accessPolicy == ShareAccessPolicy.AnyoneWithTransfer,
onClick = { onAccessPolicyChanged(ShareAccessPolicy.AnyoneWithTransfer) },
)
if (state.accessPolicy == ShareAccessPolicy.AnyoneWithTransfer) {
Text(
stringResource(Res.string.send_access_anyone_warning),
color = LocalVniDropColors.current.destructiveDefault,
style = MaterialTheme.typography.bodySmall,
)
}
if (windowClass == WindowClass.Phone) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
ShareButton(state, coreInitialized, onCreateShare, Modifier.fillMaxWidth())