rust-security について
Rust 開発者向けのセキュリティリファレンススキルです。依存関係の確認に加え、Web のリスク、認証、入力検証、unsafe コードの扱いを解説します。
主な機能
- 01cargo-auditによる依存関係監査
- 02cargo-denyによるポリシー確認
- 03SQLインジェクション対策パターン
- 04XSSと入力検証の指針
- 05JWTとパスワードハッシュの例
- 06Axumのセキュリティ設定
活用シーン
- 01CIでのRust依存関係監査
- 02データベースクエリとテンプレートの確認
- 03認証とファイルアップロードの強化
MCP 対応
READMEでは、詳細なRustセキュリティ文書の取得先として mcp__documentation__fetch_docs を案内しています。
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill rust-securityインストール後、Claude Code で/rust-securityを入力して読み込まれたか確認します。
詳細
Rust セキュリティ - クイックリファレンス
このスキルを使用しない場合
- 一般的な OWASP の概念 -
owaspまたはowasp-top-10スキルを使用してください - Java セキュリティ -
java-securityスキルを使用してください - Python セキュリティ -
python-securityスキルを使用してください - シークレット管理 -
secrets-managementスキルを使用してください
詳細な知識: Rust セキュリティのドキュメントには、technology:
rustを指定してmcp__documentation__fetch_docsを使用してください。
Rust に組み込まれたセキュリティ上の利点
Rust はデフォルトでメモリ安全性を提供します。
- null ポインターのデリファレンスが発生しません(代わりに Option を使用)
- バッファーオーバーフローが発生しません(境界チェック)
- 解放後使用が発生しません(所有権システム)
- データ競合が発生しません(借用チェッカー)
ただし、Rust は以下から保護するものではありません。
- ロジックエラー(認可の不具合)
- SQL インジェクション(文字列処理)
- XSS(テンプレート処理)
- シークレットの漏えい
- 依存関係の脆弱性
依存関係の監査
# cargo-audit - Check for known vulnerabilities
cargo install cargo-audit
cargo audit
# cargo-deny - Policy-based linting
cargo install cargo-deny
cargo deny check
# Check outdated dependencies
cargo install cargo-outdated
cargo outdated
# Snyk for Rust
snyk test
cargo-deny の設定(deny.toml)
[advisories]
vulnerability = "deny"
unmaintained = "warn"
yanked = "deny"
[licenses]
unlicensed = "deny"
allow = ["MIT", "Apache-2.0", "BSD-3-Clause"]
[bans]
multiple-versions = "warn"
wildcards = "deny"
[sources]
unknown-registry = "deny"
unknown-git = "deny"
CI/CD との統合
# GitHub Actions
- name: Security audit
run: |
cargo install cargo-audit
cargo audit
- name: Dependency policy check
run: |
cargo install cargo-deny
cargo deny check
SQL インジェクションの防止
SQLx - 安全(コンパイル時にチェック)
use sqlx::{PgPool, query_as};
// SAFE - Compile-time verified query
let user: Option<User> = sqlx::query_as!(
User,
"SELECT * FROM users WHERE email = $1",
email
)
.fetch_optional(&pool)
.await?;
// SAFE - Runtime query with bind
let user: Option<User> = sqlx::query_as::<_, User>(
"SELECT * FROM users WHERE email = $1"
)
.bind(&email)
.fetch_optional(&pool)
.await?;
Diesel - 安全(型安全な ORM)
use diesel::prelude::*;
// SAFE - Type-safe query
let user = users::table
.filter(users::email.eq(&email))
.first::<User>(&mut conn)
.optional()?;
// SAFE - Explicit parameter binding
diesel::sql_query("SELECT * FROM users WHERE email = $1")
.bind::<Text, _>(&email)
.load::<User>(&mut conn)?;
安全でないパターン
// UNSAFE - String formatting
let query = format!("SELECT * FROM users WHERE email = '{}'", email); // NEVER!
// UNSAFE - String concatenation
let query = "SELECT * FROM users WHERE email = '".to_owned() + &email + "'"; // NEVER!
XSS の防止
Askama(コンパイル時テンプレート - 自動エスケープ)
use askama::Template;
#[derive(Template)]
#[template(path = "page.html")]
struct PageTemplate<'a> {
user_input: &'a str, // Auto-escaped in template
}
<p>{{ user_input }}</p>
<p>{{ user_input|safe }}</p>
Tera(実行時テンプレート)
use tera::{Tera, Context};
let tera = Tera::new("templates/**/*")?;
let mut ctx = Context::new();
ctx.insert("user_input", &user_input); // Auto-escaped
let rendered = tera.render("page.html", &ctx)?;
ammonia による手動サニタイズ
use ammonia::clean;
// Sanitize HTML input
let safe_html = clean(&user_input);
// Custom policy
use ammonia::Builder;
let safe_html = Builder::default()
.tags(hashset!["p", "b", "i", "a"])
.url_schemes(hashset!["http", "https"])
.link_rel(Some("noopener noreferrer"))
.clean(&user_input)
.to_string();
認証 - JWT
jsonwebtoken
use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey, DecodingKey};
use serde::{Serialize, Deserialize};
use chrono::{Utc, Duration};
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String, // user_id
email: String,
exp: usize, // expiration
iat: usize, // issued at
}
fn generate_token(user_id: &str, email: &str, secret: &[u8]) -> Result<String, Error> {
let expiration = Utc::now()
.checked_add_signed(Duration::hours(1))
.expect("valid timestamp")
.timestamp() as usize;
let claims = Claims {
sub: user_id.to_owned(),
email: email.to_owned(),
exp: expiration,
iat: Utc::now().timestamp() as usize,
};
encode(
&Header::new(Algorithm::HS256),
&claims,
&EncodingKey::from_secret(secret)
)
}
fn validate_token(token: &str, secret: &[u8]) -> Result<Claims, Error> {
let mut validation = Validation::new(Algorithm::HS256);
validation.validate_exp = true;
let token_data = decode::<Claims>(
token,
&DecodingKey::from_secret(secret),
&validation
)?;
Ok(token_data.claims)
}
argon2 によるパスワードのハッシュ化
use argon2::{
password_hash::{
rand_core::OsRng,
PasswordHash, PasswordHasher, PasswordVerifier, SaltString
},
Argon2
};
fn hash_password(password: &str) -> Result<String, Error> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
Ok(argon2
.hash_password(password.as_bytes(), &salt)?
.to_string())
}
fn verify_password(password: &str, hash: &str) -> Result<bool, Error> {
let parsed_hash = PasswordHash::new(hash)?;
Ok(Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok())
}
validator による入力検証
use validator::{Validate, ValidationError};
use regex::Regex;
use lazy_static::lazy_static;
lazy_static! {
static ref NAME_REGEX: Regex = Regex::new(r"^[a-zA-Z\s\-']+$").unwrap();
}
#[derive(Debug, Validate, Deserialize)]
struct CreateUserRequest {
#[validate(email, length(max = 255))]
email: String,
#[validate(length(min = 12, max = 128), custom = "validate_password_strength")]
password: String,
#[validate(length(min = 2, max = 100), regex = "NAME_REGEX")]
name: String,
}
fn validate_password_strength(password: &str) -> Result<(), ValidationError> {
let has_upper = password.chars().any(|c| c.is_uppercase());
let has_lower = password.chars().any(|c| c.is_lowercase());
let has_digit = password.chars().any(|c| c.is_numeric());
let has_special = password.chars().any(|c| "@$!%*?&".contains(c));
if has_upper && has_lower && has_digit && has_special {
Ok(())
} else {
Err(ValidationError::new("password_strength"))
}
}
// Axum handler
async fn create_user(
Json(payload): Json<CreateUserRequest>
) -> Result<Json<User>, AppError> {
payload.validate()?;
// payload is validated
}
安全なファイルアップロード(Axum)
use axum::{
extract::Multipart,
response::Json,
};
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
use uuid::Uuid;
const MAX_FILE_SIZE: usize = 10 * 1024 * 1024; // 10 MB
const ALLOWED_TYPES: &[&str] = &["image/jpeg", "image/png", "application/pdf"];
async fn upload_file(mut multipart: Multipart) -> Result<Json<UploadResponse>, AppError> {
while let Some(field) = multipart.next_field().await? {
let content_type = field.content_type()
.ok_or(AppError::BadRequest("Missing content type"))?;
// Validate content type
if !ALLOWED_TYPES.contains(&content_type) {
return Err(AppError::BadRequest("File type not allowed"));
}
let data = field.bytes().await?;
// Validate size
if data.len() > MAX_FILE_SIZE {
return Err(AppError::BadRequest("File too large"));
}
// Generate safe filename
let ext = match content_type {
"image/jpeg" => "jpg",
"image/png" => "png",
"application/pdf" => "pdf",
_ => return Err(AppError::BadRequest("Unknown type")),
};
let safe_name = format!("{}.{}", Uuid::new_v4(), ext);
// Save file
let path = format!("uploads/{}", safe_name);
let mut file = File::create(&path).await?;
file.write_all(&data).await?;
return Ok(Json(UploadResponse { filename: safe_name }));
}
Err(AppError::BadRequest("No file provided"))
}
CORS の設定(Axum)
use tower_http::cors::{CorsLayer, Any};
use http::{HeaderValue, Method};
let cors = CorsLayer::new()
.allow_origin("https://myapp.com".parse::<HeaderValue>().unwrap())
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
.allow_headers([http::header::AUTHORIZATION, http::header::CONTENT_TYPE])
.allow_credentials(true);
let app = Router::new()
.route("/api/users", get(get_users))
.layer(cors);
セキュリティヘッダーのミドルウェア
use axum::{
middleware::{self, Next},
response::Response,
http::Request,
};
async fn security_headers<B>(request: Request<B>, next: Next<B>) -> Response {
let mut response = next.run(request).await;
let headers = response.headers_mut();
headers.insert("X-Content-Type-Options", "nosniff".parse().unwrap());
headers.insert("X-Frame-Options", "DENY".parse().unwrap());
headers.insert("X-XSS-Protection", "0".parse().unwrap());
headers.insert("Referrer-Policy", "strict-origin-when-cross-origin".parse().unwrap());
headers.insert("Content-Security-Policy", "default-src 'self'".parse().unwrap());
headers.insert(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains".parse().unwrap()
);
response
}
// Apply to router
let app = Router::new()
.route("/", get(index))
.layer(middleware::from_fn(security_headers));
レート制限
use governor::{Quota, RateLimiter};
use nonzero_ext::nonzero;
use std::sync::Arc;
// Create rate limiter
let limiter = Arc::new(RateLimiter::direct(
Quota::per_minute(nonzero!(10u32))
));
// Middleware
async fn rate_limit<B>(
State(limiter): State<Arc<RateLimiter<...>>>,
request: Request<B>,
next: Next<B>
) -> Result<Response, StatusCode> {
match limiter.check() {
Ok(_) => Ok(next.run(request).await),
Err(_) => Err(StatusCode::TOO_MANY_REQUESTS),
}
}
シークレット管理
use std::env;
#[derive(Clone)]
struct Config {
jwt_secret: String,
database_url: String,
api_key: String,
}
impl Config {
fn from_env() -> Result<Self, ConfigError> {
Ok(Config {
jwt_secret: env::var("JWT_SECRET")
.map_err(|_| ConfigError::Missing("JWT_SECRET"))?,
database_url: env::var("DATABASE_URL")
.map_err(|_| ConfigError::Missing("DATABASE_URL"))?,
api_key: env::var("API_KEY")
.map_err(|_| ConfigError::Missing("API_KEY"))?,
})
}
}
// NEVER hardcode secrets
// const JWT_SECRET: &str = "hardcoded-secret"; // NEVER!
セキュリティイベントのロギング
use tracing::{info, warn};
fn log_login_attempt(username: &str, success: bool, ip: &str) {
info!(
user = username,
success = success,
ip = ip,
"login attempt"
);
}
fn log_access_denied(user_id: &str, resource: &str, ip: &str) {
warn!(
user_id = user_id,
resource = resource,
ip = ip,
"access denied"
);
}
// NEVER log sensitive data
// info!(password = password, "user data"); // NEVER!
unsafe コードのガイドライン
// Minimize unsafe blocks
// Document why unsafe is necessary
// Encapsulate unsafe in safe abstractions
/// SAFETY: buffer is guaranteed to be valid UTF-8
/// because it was created from a valid String
unsafe fn process_buffer(buffer: &[u8]) -> &str {
std::str::from_utf8_unchecked(buffer)
}
// Prefer safe alternatives
let s = std::str::from_utf8(buffer)?; // Safe version
アンチパターン
| アンチパターン | 問題点 | 適切な方法 |
|---|---|---|
SQL クエリ内の format! | SQL インジェクション | バインドを使用するクエリマクロを使用する |
| ユーザー入力への ` | safe` フィルター | XSS の脆弱性 |
| シークレットのハードコード | シークレットの漏えい | 環境変数を使用する |
過剰な unsafe ブロック | メモリ安全性の回避 | unsafe を最小限にして、ドキュメント化する |
cargo audit の警告を無視する | 既知の脆弱性 | 依存関係を更新または置き換える |
| 弱い JWT アルゴリズム | トークンの偽造 | 最低でも HS256 を使用する |
ハンドラー内の unwrap() | 本番環境でのパニック | 適切なエラー処理を使用する |
クイックトラブルシューティング
| 問題 | 考えられる原因 | 解決方法 |
|---|---|---|
| cargo audit が RUSTSEC を検出する | 脆弱なクレート | cargo update で更新する |
| JWT の検証に失敗する | アルゴリズムまたはキーが間違っている | Algorithm enum とキーを確認する |
| CORS エラー | オリジンが設定されていない | CorsLayer にオリジンを追加する |
| パスワードハッシュの処理が遅い | Argon2 のパラメーターが高すぎる | メモリまたは反復回数を調整する |
| SQLx のコンパイルエラー | クエリがスキーマと一致しない | cargo sqlx prepare を実行する |
| テンプレートがエスケープしない | ` | safe` フィルターを使用している |
セキュリティスキャンのコマンド
# Vulnerability audit
cargo audit
# Policy check
cargo deny check
# Clippy security lints
cargo clippy -- -W clippy::all -W clippy::pedantic
# Check for secrets
gitleaks detect
trufflehog git file://.
# SAST with semgrep
semgrep --config=p/rust .
関連スキル
コメント
Markdown 対応