actix_modsecurity/
modsecurity.rs

1use std::path::Path;
2
3use actix_http::Response;
4use actix_web::{
5    HttpMessage, HttpRequest, HttpResponse,
6    body::{BodyStream, BoxBody, to_bytes_limited},
7    dev::{Payload, ServiceRequest},
8    http::{StatusCode, Version, header},
9};
10
11use crate::{error::Error, factory::Middleware};
12
13const CONNECTION_INFO: &str = concat!(env!("CARGO_PKG_NAME"), " ", env!("CARGO_PKG_VERSION"));
14
15pub type Addr = (String, u16);
16
17#[derive(Clone, Default)]
18struct TransactionConfig {
19    max_request_body: Option<usize>,
20    max_response_body: Option<usize>,
21    server_address: Option<Addr>,
22}
23
24/// Actix-Web compatible wrapper on [`ModSecurity`](modsecurity::ModSecurity)
25pub struct ModSecurity {
26    config: TransactionConfig,
27    rules: modsecurity::Rules,
28    security: modsecurity::ModSecurity,
29}
30
31impl ModSecurity {
32    /// Creates a new [`ModSecurity`](crate::ModSecurity) instance.
33    ///
34    /// Because of implementation specifics of LibModSecurity, it is
35    /// recommended only once instance exist within the program.
36    ///
37    /// See [`modsecurity::msc::ModSecurity`](modsecurity::msc::ModSecurity)
38    /// for more details.
39    pub fn new() -> Self {
40        Self {
41            config: TransactionConfig::default(),
42            rules: modsecurity::Rules::new(),
43            security: modsecurity::ModSecurity::builder()
44                .with_connector_info(CONNECTION_INFO)
45                .expect("failed to add connector into")
46                .build(),
47        }
48    }
49
50    /// Adds plain rules from string into the set.
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// use actix_modsecurity::ModSecurity;
56    ///
57    /// let mut security = ModSecurity::new();
58    /// security.add_rules("SecRuleEngine On\n").expect("Failed to add rules");
59    /// ```
60    pub fn add_rules(&mut self, rules: &str) -> Result<&mut Self, Error> {
61        self.rules.add_plain(rules)?;
62        Ok(self)
63    }
64
65    /// Adds rules from a file into the set.
66    ///
67    /// # Examples
68    ///
69    /// ```no_run
70    /// use actix_modsecurity::ModSecurity;
71    ///
72    /// let mut security = ModSecurity::new();
73    /// security.add_rules_file("/path/to/rules.conf").expect("Failed to add rules from file");
74    /// ```
75    pub fn add_rules_file<P: AsRef<Path>>(&mut self, file: P) -> Result<&mut Self, Error> {
76        self.rules.add_file(file)?;
77        Ok(self)
78    }
79
80    /// Configure Max request body size allowed to be loaded into memory for processing.
81    ///
82    /// This avoids out-of-memory errors and potential security-risks from attackers
83    /// overloading your web-service.
84    pub fn set_max_request_size(&mut self, max_request_body: Option<usize>) -> &mut Self {
85        self.config.max_request_body = max_request_body;
86        self
87    }
88
89    /// Configure Max response body size allowed to be loaded into memory for processing.
90    ///
91    /// This avoids out-of-memory errors and potential security-risks from attackers
92    /// overloading your web-service.
93    pub fn set_max_response_size(&mut self, max_response_body: Option<usize>) -> &mut Self {
94        self.config.max_response_body = max_response_body;
95        self
96    }
97
98    /// Include server bindings information to include in transaction processing.
99    ///
100    /// Allows [`Transaction::process_connection`](crate::Transaction::process_connection)
101    /// to work as intended rather than skip over connection information.
102    pub fn set_server_address(&mut self, server_address: Option<Addr>) -> &mut Self {
103        self.config.server_address = server_address;
104        self
105    }
106
107    /// Creates a configured LibModSecurity Transaction with the configured rules.
108    pub fn transaction(&self) -> Result<Transaction, Error> {
109        Ok(Transaction {
110            config: self.config.clone(),
111            transaction: self
112                .security
113                .transaction_builder()
114                .with_rules(&self.rules)
115                .build()?,
116        })
117    }
118
119    /// Converts ModSecurity Instance into Actix-Web Middleware
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// use actix_web::App;
125    /// use actix_modsecurity::ModSecurity;
126    ///
127    /// let mut security = ModSecurity::new();
128    /// security.add_rules("SecRuleEngine On\n").expect("Failed to add rules");
129    ///
130    /// let app = App::new()
131    ///     .wrap(security.middleware());
132    /// ```
133    #[inline]
134    pub fn middleware(self) -> Middleware {
135        self.into()
136    }
137}
138
139impl Into<Middleware> for ModSecurity {
140    #[inline]
141    fn into(self) -> Middleware {
142        Middleware::new(self)
143    }
144}
145
146#[inline]
147fn version_str(v: Version) -> &'static str {
148    match v {
149        Version::HTTP_09 => "0.9",
150        Version::HTTP_10 => "1.0",
151        Version::HTTP_11 => "1.1",
152        Version::HTTP_2 => "2",
153        Version::HTTP_3 => "3",
154        _ => panic!("unexpected http version!"),
155    }
156}
157
158#[inline]
159fn intervention_response(intv: &modsecurity::Intervention) -> Result<HttpResponse, Error> {
160    if let Some(log) = intv.log() {
161        tracing::error!("{log}");
162    }
163    if let Some(url) = intv.url() {
164        let mut res = HttpResponse::TemporaryRedirect();
165        res.insert_header((header::LOCATION, url));
166        return Ok(res.into());
167    }
168    let code = StatusCode::from_u16(intv.status() as u16)?;
169    return Ok(HttpResponse::new(code));
170}
171
172/// Actix-Web compatible wrapper on [`Tranaction`](modsecurity::Transaction)
173pub struct Transaction<'a> {
174    config: TransactionConfig,
175    transaction: modsecurity::Transaction<'a>,
176}
177
178impl<'a> Transaction<'a> {
179    /// Performs analysis on the connection.
180    ///
181    /// This should be called at the very beginning of a request process.
182    ///
183    /// **NOTE**: Remember to check for a possible intervention using
184    /// [`Transaction::intervention()`] after calling this method.
185    pub fn process_connection(&mut self, req: &HttpRequest) -> Result<(), Error> {
186        let Some(caddr) = req.peer_addr() else {
187            tracing::warn!("missing client-address. cannot scan connection");
188            return Ok(());
189        };
190        let Some(saddr) = self.config.server_address.as_ref() else {
191            tracing::warn!("missing server-address. cannot scan connection");
192            return Ok(());
193        };
194        Ok(self.transaction.process_connection(
195            &caddr.ip().to_string(),
196            caddr.port() as i32,
197            &saddr.0,
198            saddr.1 as i32,
199        )?)
200    }
201
202    /// Perform the analysis on the URI and all the query string variables.
203    ///
204    /// This should be called at the very beginning of a request process.
205    ///
206    /// **NOTE**: Remember to check for a possible intervention using
207    /// [`Transaction::intervention()`] after calling this method.
208    #[inline]
209    pub fn process_uri(&mut self, req: &HttpRequest) -> Result<(), Error> {
210        Ok(self.transaction.process_uri(
211            &req.uri().to_string(),
212            req.method().as_str(),
213            version_str(req.version()),
214        )?)
215    }
216
217    /// Processes rules in the request headers phase for this transaction.
218    ///
219    /// This should be called at the very beginning of a request process.
220    ///
221    /// **NOTE**: Remember to check for a possible intervention using
222    /// [`Transaction::intervention()`] after calling this method.
223    #[inline]
224    pub fn process_request_headers(&mut self, req: &HttpRequest) -> Result<(), Error> {
225        req.headers()
226            .iter()
227            .filter_map(|(k, v)| Some((k.as_str(), v.to_str().ok()?)))
228            .try_for_each(|(k, v)| self.transaction.add_request_header(k, v))?;
229        Ok(self.transaction.process_request_headers()?)
230    }
231
232    /// Processes rules in the request body phase for this transaction.
233    ///
234    /// This should be called at the very beginning of a request process.
235    ///
236    /// **NOTE**: Remember to check for a possible intervention using
237    /// [`Transaction::intervention()`] after calling this method.
238    pub async fn process_request_body(&mut self, payload: Payload) -> Result<Payload, Error> {
239        let max = self.config.max_request_body.unwrap_or(u16::MAX as usize);
240        let stream = BodyStream::new(payload);
241        let body = to_bytes_limited(stream, max).await??;
242        self.transaction.append_request_body(&body)?;
243        self.transaction.process_request_body()?;
244
245        let (_, mut payload) = actix_http::h1::Payload::create(true);
246        payload.unread_data(body);
247        Ok(Payload::H1 { payload })
248    }
249
250    /// Processes *ALL* rules in the request phase for this transaction.
251    ///
252    /// This should be called at the very beginning of a request process.
253    /// Use this instead of any of the following:
254    ///
255    ///  - [`Transaction::process_connection`]
256    ///  - [`Transaction::process_uri`]
257    ///  - [`Transaction::process_request_headers`]
258    ///  - [`Transaction::process_request_body`]
259    ///
260    /// **NOTE**: Remember to check for a possible intervention using
261    /// [`Transaction::intervention()`] after calling this method.
262    pub async fn process_request(&mut self, req: &mut ServiceRequest) -> Result<(), Error> {
263        self.process_connection(req.request())?;
264        self.process_uri(req.request())?;
265        self.process_request_headers(req.request())?;
266        let payload = self.process_request_body(req.take_payload()).await?;
267        req.set_payload(payload);
268        Ok(())
269    }
270
271    /// Processes rules in the response headers phase for this transaction.
272    ///
273    /// **NOTE**: Remember to check for a possible intervention using
274    /// [`Transaction::intervention()`] after calling this method.
275    pub fn process_response_headers<T>(&mut self, res: &HttpResponse<T>) -> Result<(), Error> {
276        let code: u16 = res.status().into();
277        let version = format!("HTTP {}", version_str(res.head().version));
278        res.headers()
279            .iter()
280            .filter_map(|(k, v)| Some((k.as_str(), v.to_str().ok()?)))
281            .try_for_each(|(k, v)| self.transaction.add_response_header(k, v))?;
282        Ok(self
283            .transaction
284            .process_response_headers(code as i32, &version)?)
285    }
286
287    /// Processes rules in the response body phase for this transaction.
288    ///
289    /// **NOTE**: Remember to check for a possible intervention using
290    /// [`Transaction::intervention()`] after calling this method.
291    pub async fn process_response_body(&mut self, body: BoxBody) -> Result<BoxBody, Error> {
292        let max = self.config.max_response_body.unwrap_or(u16::MAX as usize);
293        let body = to_bytes_limited(body, max).await??;
294        self.transaction.append_response_body(&body)?;
295        self.transaction.process_response_body()?;
296        Ok(BoxBody::new(body))
297    }
298
299    /// Processes *ALL* rules in the response phase for this transaction.
300    ///
301    /// This should be called at the very beginning of a request process.
302    /// Use this instead of any of the following:
303    ///
304    ///  - [`Transaction::process_response_headers`]
305    ///  - [`Transaction::process_response_body`]
306    ///
307    /// **NOTE**: Remember to check for a possible intervention using
308    /// [`Transaction::intervention()`] after calling this method.
309    pub async fn process_response(&mut self, res: HttpResponse) -> Result<HttpResponse, Error> {
310        let (http_res, mut body) = res.into_parts();
311        self.process_response_headers(&http_res)?;
312        body = self.process_response_body(body).await?;
313        Ok(http_res.set_body(body))
314    }
315
316    /// Returns an intervention if one is triggered by the transaction.
317    ///
318    /// An intervention is triggered when a rule is matched and the
319    /// corresponding action is disruptive.
320    pub fn intervention(&mut self) -> Result<Option<Intervention>, Error> {
321        let Some(intv) = self.transaction.intervention() else {
322            return Ok(None);
323        };
324        let response = intervention_response(&intv)?;
325        Ok(Some(Intervention {
326            message: intv.log().map(|s| s.to_owned()),
327            url: intv.url().map(|u| u.to_owned()),
328            code: StatusCode::from_u16(intv.status() as u16)?,
329            response,
330        }))
331    }
332}
333
334/// Actix-Web compatible wrapper on
335/// [`Intervention`](modsecurity::intervention::Intervention)
336#[derive(Debug)]
337pub struct Intervention {
338    message: Option<String>,
339    url: Option<String>,
340    code: StatusCode,
341    response: HttpResponse<BoxBody>,
342}
343
344impl Intervention {
345    /// Returns the log message, if any, of the intervention.
346    #[inline]
347    pub fn log(&self) -> Option<&str> {
348        self.message.as_ref().map(|s| s.as_str())
349    }
350
351    /// Returns the URL, if any, of the intervention.
352    #[inline]
353    pub fn url(&self) -> Option<&str> {
354        self.url.as_ref().map(|s| s.as_str())
355    }
356
357    /// Returns the status code of the intervention.
358    #[inline]
359    pub fn status(&self) -> StatusCode {
360        self.code
361    }
362
363    /// Returns the repacement HttpResponse of the intervention
364    pub fn response(&self) -> &HttpResponse {
365        &self.response
366    }
367}
368
369impl Into<HttpResponse> for Intervention {
370    fn into(self) -> HttpResponse {
371        self.response
372    }
373}
374
375impl Into<Response<BoxBody>> for Intervention {
376    fn into(self) -> Response<BoxBody> {
377        self.response.into()
378    }
379}