mcpField
R
rust-security

rust-security

github.com/claude-dev-suiteセキュリティ4644 回インストール3 日前に更新
リポジトリ

rust-security について

Rust 開発者向けのセキュリティリファレンススキルです。依存関係の確認に加え、Web のリスク、認証、入力検証、unsafe コードの扱いを解説します。

主な機能

  1. 01cargo-auditによる依存関係監査
  2. 02cargo-denyによるポリシー確認
  3. 03SQLインジェクション対策パターン
  4. 04XSSと入力検証の指針
  5. 05JWTとパスワードハッシュの例
  6. 06Axumのセキュリティ設定

活用シーン

  1. 01CIでのRust依存関係監査
  2. 02データベースクエリとテンプレートの確認
  3. 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 .

関連スキル

コメント