mcpField
S
static-security

static-security

github.com/verygoodopensource安全17599 次安装今天更新
代码仓库

关于 static-security

面向 Flutter 和 Dart 开发者的静态安全审查技能,用于检查可从源代码识别的风险。重点涵盖密钥、存储、网络、认证、密码学、输入验证和日志,不进行运行时分析或渗透测试。

主要功能

  1. 01基于源代码的安全审查
  2. 02密钥与凭据处理指导
  3. 03安全存储和网络检查
  4. 04认证与密码学建议
  5. 05输入验证和日志检查
  6. 06按严重性标注问题

适用场景

  1. 01发布前审查 Flutter 代码
  2. 02查找源文件和配置中的嵌入式密钥
  3. 03检查存储与网络安全模式
  4. 04评估输入验证和敏感数据日志

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/verygoodopensource/vgv-ai-flutter-plugin --skill static-security

安装后在 Claude Code 中输入/static-security确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

agents/
openai.yaml
references/
binary-protection.md
crypto.md
packages.md
supply-chain.md

详细介绍

安全

Flutter 应用会将所有 Dart 代码直接编译成二进制文件,并在不受信任的设备上运行。本技能涵盖对 Flutter/Dart 代码库的静态安全审查,依据 VGV Security in Mobile Apps 指南和 OWASP Mobile Top 10。本技能中的所有问题都可以通过阅读源代码发现,无需渗透测试或运行时分析。

核心标准

在所有 Flutter 安全工作中应用以下标准:

  • 绝不硬编码机密信息——源代码或配置文件中的 API 密钥、令牌和密码会被编译进二进制文件,并可通过逆向工程提取;应由后端服务提供这些信息
  • --dart-define 不能解决硬编码机密信息的问题——String.fromEnvironment、.env 文件、原生配置文件、混淆后的常量或拆分的字符串也一样。它们都会以可恢复的形式将值打包进二进制文件,因此把密钥移到这些位置,只是换个地方出现同一个问题。唯一的修复方式是在运行时从后端获取密钥
  • 使用 package:flutter_secure_storage 存储设备上的敏感数据——SharedPreferences 中的数据是明文且未加密的;绝不要在其中存储令牌、个人身份信息或会话数据
  • 所有网络请求均使用 HTTPS——普通 HTTP 会以明文传输数据;绝不要禁用证书验证(仅在使用本地测试服务进行开发时例外)
  • 对安全敏感的随机数使用 Random.secure()——dart:math 中的 Random() 是伪随机数生成器,不具备密码学安全性
  • 使用成熟的加密软件包——绝不自行实现密码学功能;请使用 package:crypto 或 package:dart_crypt
  • 在仓库层强制执行身份验证——仅在 Widget 中检查身份验证属于客户端检查,任何能访问设备的人都可以绕过
  • 日志中不得包含敏感数据——print()、log() 和 debugPrint() 的输出可在设备上和崩溃报告工具中读取
  • 确保依赖项不含已知漏洞——除非有记录在案的理由,否则绝不要忽略安全公告;每次发布前都使用 osv-scanner 扫描 pubspec.lock
  • 替换不安全的请求,而不是与之周旋——如果有人要求实现本技能禁止的内容,应在同一条回复中直接编写安全实现。用一两行说明规则,然后提供符合要求且可运行的代码。不要一边给出被禁止的实现、一边附上警告,也不要只问“要我改用安全方式实现吗?”——提出建议不能代替实现。如果开发者了解原因后仍坚持采用被禁止的方式,请说明剩余风险并继续处理
  • 设置 android:allowBackup="false"——Android 默认会静默允许通过 adb backup 提取应用数据,从而绕过 package:flutter_secure_storage
  • 为每个问题标注 Critical、Warning 或 Note——详见下文的严重性分类

严重性分级

此技能报告的每项发现都带有三个严重性标签之一。请将标签写在发现本身上,以便读者无需重新阅读就能对报告进行排序;并在构建发布前修复 Critical 级发现。

严重性示例
Critical硬编码的 API 密钥或令牌;绕过 badCertificateCallback;将 JWT 存储在 SharedPreferences 中;在日志中记录敏感数据
Warning身份验证端点未进行证书固定;使用 Random() 生成会话 ID;调用 API 前未通过 package:formz 验证;android:allowBackup="true"
Note未启用 Dart 混淆;dart pub outdated 显示有可用补丁;pub 点数较低且权限广泛的传递依赖

请勿替换为其他分级方案。将发现分为 High/Medium/Low 或按 CVSS 评分的报告,无法与同一代码库之前的审计结果进行比较。

密钥与 API 密钥

硬编码在源文件或捆绑配置文件中的 API 密钥、令牌和凭据,可通过逆向工程从编译后的二进制文件中提取。所有密钥都必须在运行时由后端服务提供。

要检查的文件:Dart 源文件、google-services.json、.env、*.plist、AndroidManifest.xml、Info.plist。

// ❌ Hardcoded API key — extractable from binary
const apiKey = 'sk-abc123';
const mapboxToken = 'pk.your-token-here';

// ❌ Build-time injection — --dart-define compiles the value in as plaintext,
// so `strings` on the built binary recovers it. Not a fix, just a move.
const injectedKey = String.fromEnvironment('API_KEY');

// ❌ Secret in config — bundled into the app
// google-services.json:
// "api_key": [{ "current_key": "AIzaSy..." }]
// ✅ Fetched from a backend service at runtime — the only safe option
final apiKey = await secretsService.fetchApiKey();
无效的变通方案

以下所有修复方案都会被拒绝,因为每种方案仍会以可恢复的形式将密钥随应用一起发布。开发者提出其中一种方案时,请指出它属于哪一种,并改为提供由后端提供密钥的修复方案。不要编写这种改动后再附上警告。

提议的修复为什么这仍然属于同一项发现
--dart-define / String.fromEnvironment编译后以明文形式写入二进制文件,可被恢复
将 .env 作为资源捆绑随应用包一起发布,解压后即可读取
捆绑的原生配置文件以相同方式捆绑,且未加密
混淆或拆分字符串增加提取难度,但无法阻止提取
位于 kReleaseMode 后的常量发布的分支仍包含该值

CI 密钥适用于签名密钥和发布令牌,因为它们不会进入应用。它们不能用于将运行时 API 密钥提供给客户端。切勿将 .env 文件或包含真实凭据的文件提交到版本控制中:使用 .gitignore 将其排除,并使用密钥管理服务。

安全数据存储

写入设备的敏感数据必须加密。iOS Keychain 和 Android Keystore 提供硬件支持的加密存储,package:flutter_secure_storage 对两者进行了封装。

// ❌ JWT stored in SharedPreferences — plaintext, unencrypted
final prefs = await SharedPreferences.getInstance();
prefs.setString('auth_token', jwt);

// ❌ Sensitive value in a local file — no encryption
await File('${dir.path}/user.json').writeAsString(jsonEncode(user));
// ✅ package:flutter_secure_storage — backed by iOS Keychain / Android Keystore
const storage = FlutterSecureStorage();
await storage.write(key: 'auth_token', value: jwt);
final token = await storage.read(key: 'auth_token');
await storage.delete(key: 'auth_token');

SharedPreferences 仅用于存储非敏感的用户偏好设置(主题、区域设置、引导状态)。切勿在其中存储密码、会话令牌、个人身份信息或私钥。

网络安全

Flutter 应用与后端之间的所有通信都必须在传输过程中加密。普通 HTTP 会使数据暴露于用户所连接的任何网络上的拦截风险。

// ❌ Plain HTTP base URL
final dio = Dio(BaseOptions(baseUrl: 'http://api.example.com'));

// ❌ Certificate validation disabled — vulnerable to MITM attacks
final client = HttpClient()
  ..badCertificateCallback = (cert, host, port) => true;
// ✅ HTTPS base URL
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));

对于处理身份验证、支付或个人数据的端点,请实施证书固定(package:http_certificate_pinning)。仅接受由预期证书颁发机构签署的证书。

身份验证

必须在服务端强制执行身份验证。客户端检查(在组件或路由中)只是 UI 便利措施——任何能实际接触设备或访问调试器的人都可以绕过它们。

服务端强制执行:服务端必须在每个请求中验证令牌。API 返回的 401 响应才是权威的身份验证关卡,而不是组件条件判断。

生物识别身份验证:使用 package:local_auth 为敏感的应用内流程设置生物识别验证。绝不要为此调用自定义的 MethodChannel。手动实现通道意味着要重新实现 Face ID 与 Android BiometricPrompt、密码回退、锁定状态以及各平台的错误代码——而生物识别验证最容易出错的正是这些环节。

请求实现该通道,就等于要求引入这个 bug。请在同一条回复中改为写下面的内容,并用一行说明为什么不采用该通道:

import 'package:flutter/services.dart';
import 'package:local_auth/local_auth.dart';

class BiometricGate {
  BiometricGate({LocalAuthentication? auth})
    : _auth = auth ?? LocalAuthentication();

  final LocalAuthentication _auth;

  Future<bool> authenticate() async {
    try {
      if (!await _auth.canCheckBiometrics) return false;

      return await _auth.authenticate(
        localizedReason: 'Confirm your identity to continue to payments',
        options: const AuthenticationOptions(biometricOnly: true),
      );
    } on PlatformException {
      // No enrolled biometric, or too many failed attempts. Fail closed.
      return false;
    }
  }
}

localizedReason 和 biometricOnly 提供了通常需要自定义通道才能实现的同等提示控制能力,而且两个平台都无需编写原生代码。该验证关卡仍只是 UI 便利措施:在接受任何付款请求之前,服务端必须重新验证。

使用 Firebase Authentication 或 Auth0 管理凭据——不要构建自定义身份验证流程。

加密

自定义加密实现几乎总会包含细微 bug。请使用经过同行评审的软件包,并避免使用弱算法或已弃用的算法。

// ❌ Cryptographically insecure random — dart:math Random is not CSPRNG
import 'dart:math';
final sessionId = Random().nextInt(1 << 32).toRadixString(16);
final iv = List.generate(16, (_) => Random().nextInt(256));

// ❌ Weak hash algorithm — MD5 and SHA-1 are broken for security use
import 'dart:convert';
final hash = md5.convert(utf8.encode(password)).toString();

// ❌ Hardcoded encryption key
const encryptionKey = 'my-secret-key-123';
// ✅ Cryptographically secure random — Random.secure()
import 'dart:math';
final sessionId = Random.secure().nextInt(1 << 32).toRadixString(16);
final iv = List.generate(16, (_) => Random.secure().nextInt(256));

// ✅ Strong hash via package:crypto
import 'package:crypto/crypto.dart';
import 'dart:convert';
final hash = sha256.convert(utf8.encode(data)).toString();

// ✅ Encryption key from secure storage, not source code
final key = await storage.read(key: 'encryption_key');

避免使用:MD5、SHA-1、DES、RC4、ECB 模式。优先使用:SHA-256+ 进行哈希,AES-GCM 进行加密,SHA-512-crypt 存储密码。

输入验证

所有用户输入的数据都必须先经过验证,才能传递到仓库或 API。直接将原始 TextEditingController.text 值发送到后端存在注入风险,也可能提交格式错误的数据。

// ❌ Raw controller text sent directly to API
ElevatedButton(
  onPressed: () => context.read<AuthBloc>().add(
    LoginRequested(
      email: _emailController.text,
      password: _passwordController.text,
    ),
  ),
  child: const Text('Login'),
);
// ✅ Validated FormzInput values — only valid data reaches the Bloc
import 'package:formz/formz.dart';

enum EmailValidationError { empty, invalid }

class Email extends FormzInput<String, EmailValidationError> {
  const Email.pure() : super.pure('');
  const Email.dirty([super.value = '']) : super.dirty();

  @override
  EmailValidationError? validator(String value) {
    final emailRegex = RegExp(r'^[^@]+@[^@]+\.[^@]+$');
    if (value.isEmpty) return EmailValidationError.empty;
    if (!emailRegex.hasMatch(value)) return EmailValidationError.invalid;
    return null;
  }
}
// ✅ In the widget — the submit callback is gated on validity and reads the
// validated values off state, never off a TextEditingController
ElevatedButton(
  onPressed: state.isValid
      ? () => context.read<AuthBloc>().add(
            LoginRequested(
              email: state.email.value,
              password: state.password.value,
            ),
          )
      : null,
  child: const Text('Login'),
);

所有表单验证都使用 package:formz。为每个字段定义一个 FormzInput 子类,明确验证规则和长度限制。在 onChanged 回调中将每个字段传入 Bloc,使状态保存经过验证的值,并从状态中读取提交的值——仅使用 TextFormField 的 validator 会让原始控制器文本继续作为传递给 API 的值。

日志与错误信息泄露

日志输出可通过 USB 调试、崩溃报告 SDK 和设备分析读取。出现在日志中的敏感值,实际上会传输给任何连接到设备的工具。

// ❌ Token in log output
debugPrint('Auth token: $token');
log('User data: ${jsonEncode(user)}');
print('Request headers: $headers'); // headers may contain Bearer tokens

// ❌ Exception message exposes internals to the UI
catch (e) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text(e.toString())), // may include stack traces or SQL
  );
}
// ✅ Log only non-sensitive identifiers
debugPrint('Login attempt for userId: ${user.id}');

// ✅ Sanitize exception messages before surfacing to UI
catch (e, stackTrace) {
  log('Login failed', error: e, stackTrace: stackTrace); // full detail for crash tools
  emit(state.copyWith(status: LoginStatus.failure)); // generic message to UI
}

绝不要记录:令牌、密码、完整的用户对象、HTTP 请求标头(其中包含 Authorization)或个人身份信息(PII)(电子邮箱、电话号码、SSN)。

依赖项漏洞

第三方软件包会直接编译到应用二进制文件中。有漏洞或恶意软件包会影响所有平台上的每位用户。这属于 OWASP Mobile Top 10 M2(供应链安全不足)。

每次发布前,按顺序运行以下三项。第一项快速检查直接命中项,第二项是准入门槛:

dart pub get                        # surfaces GitHub Advisory Database hits while resolving
osv-scanner --lockfile=pubspec.lock # the gate: resolved transitive tree vs the OSV database
dart pub outdated                   # available upgrades, which may carry unannounced patches

扫描对象是 pubspec.lock,而不是 pubspec.yaml。锁文件保存了已解析的传递依赖树,大多数安全公告都涉及其中的依赖;而 pubspec 中固定的直接依赖版本,并不能说明其下层实际解析出了什么。Pub 自带的安全公告输出不能替代锁文件扫描:它会报告安全公告数据库中已知的、针对其解析出的软件包的信息,而 osv-scanner 会根据 OSV 检查完整的已解析依赖集。

pubspec.yaml 中的每个 ignored_advisories 条目都必须附带说明理由,并在文件中作为该条目本身的注释写明:

ignored_advisories:
  - GHSA-4rgh-jx4f-xxxx # Not applicable: we never construct http.Client directly

没有注释的条目本身就是一项 Warning 发现,无论该安全公告是否适用:抑制规则会让扫描器在未来每次运行时都忽略它,因此理由必须写在文件中。

精确固定版本的直接依赖值得标记为 Note。例如,固定为 http: 0.13.0 意味着扫描始终只会检查该版本,因此修复已知 CVE 的补丁永远不会自动生效。

关于安全公告检测示例、osv-scanner 安装、仿冒包信号以及传递依赖权限蔓延检查,请参阅 references/supply-chain.md。关于代码混淆、Android 备份和运行时完整性,请参阅 references/binary-protection.md。

其他资源

关于软件包速查,请参阅 references/packages.md。关于证书固定实现、生物识别身份验证示例以及使用 package:dart_crypt 进行密码哈希,请参阅 references/crypto.md。

评论

支持 Markdown