mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
feat: make policy length limits configurable per engine (#624)
- Add PolicyLengthConfig struct with max_col, max_file_bytes, and max_lines fields, replacing hardcoded constants in the lexer. - Add Engine::set_policy_length_config and clear_policy_length_config to allow callers to override the default limits. - Add Source::from_contents_with_limits and from_file_with_limits for direct Source construction with custom limits; existing from_contents and from_file signatures are preserved using defaults. - Add tests for default rejection, custom limits, and engine plumbing. - Add bindings for C, C++, Python, WASM/JS, Java, Ruby, C#, Go
This commit is contained in:
@@ -9,6 +9,7 @@ use crate::lexer::*;
|
||||
use crate::parser::*;
|
||||
use crate::scheduler::*;
|
||||
use crate::utils::gather_functions;
|
||||
use crate::utils::limits::PolicyLengthConfig;
|
||||
use crate::utils::limits::{self, fallback_execution_timer_config, ExecutionTimerConfig};
|
||||
use crate::value::*;
|
||||
use crate::*;
|
||||
@@ -26,6 +27,7 @@ pub struct Engine {
|
||||
prepared: bool,
|
||||
rego_v1: bool,
|
||||
execution_timer_config: Option<ExecutionTimerConfig>,
|
||||
policy_length_config: PolicyLengthConfig,
|
||||
}
|
||||
|
||||
#[cfg(feature = "azure_policy")]
|
||||
@@ -83,6 +85,7 @@ impl Engine {
|
||||
prepared: false,
|
||||
rego_v1: true,
|
||||
execution_timer_config: None,
|
||||
policy_length_config: PolicyLengthConfig::default(),
|
||||
};
|
||||
engine.apply_effective_execution_timer_config();
|
||||
engine
|
||||
@@ -144,6 +147,31 @@ impl Engine {
|
||||
self.interpreter.set_execution_timer_config(Some(config));
|
||||
}
|
||||
|
||||
/// Set the policy length limits used when loading policies.
|
||||
///
|
||||
/// Controls maximum file size, line count, and column width for policy files.
|
||||
/// Engines start with the default limits defined by [`PolicyLengthConfig::default`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use core::num::{NonZeroU32, NonZeroUsize};
|
||||
/// use regorus::utils::limits::PolicyLengthConfig;
|
||||
/// use regorus::Engine;
|
||||
///
|
||||
/// let mut engine = Engine::new();
|
||||
/// let config = PolicyLengthConfig {
|
||||
/// max_col: NonZeroU32::new(2048).unwrap(),
|
||||
/// max_file_bytes: NonZeroUsize::new(2_097_152).unwrap(),
|
||||
/// max_lines: NonZeroUsize::new(40_000).unwrap(),
|
||||
/// };
|
||||
///
|
||||
/// engine.set_policy_length_config(config);
|
||||
/// ```
|
||||
pub const fn set_policy_length_config(&mut self, config: PolicyLengthConfig) {
|
||||
self.policy_length_config = config;
|
||||
}
|
||||
|
||||
/// Clear the engine-specific execution timer configuration, falling back to the global value.
|
||||
///
|
||||
/// # Examples
|
||||
@@ -171,6 +199,20 @@ impl Engine {
|
||||
self.apply_effective_execution_timer_config();
|
||||
}
|
||||
|
||||
/// Clear the policy length configuration, reverting to the defaults.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use regorus::Engine;
|
||||
///
|
||||
/// let mut engine = Engine::new();
|
||||
/// engine.clear_policy_length_config();
|
||||
/// ```
|
||||
pub fn clear_policy_length_config(&mut self) {
|
||||
self.policy_length_config = PolicyLengthConfig::default();
|
||||
}
|
||||
|
||||
/// Add a policy.
|
||||
///
|
||||
/// The policy file will be parsed and converted to AST representation.
|
||||
@@ -198,7 +240,12 @@ impl Engine {
|
||||
/// ```
|
||||
///
|
||||
pub fn add_policy(&mut self, path: String, rego: String) -> Result<String> {
|
||||
let source = Source::from_contents(path, rego)?;
|
||||
let source = Source::from_contents_with_limits(
|
||||
path,
|
||||
rego,
|
||||
self.policy_length_config.max_file_bytes,
|
||||
self.policy_length_config.max_lines,
|
||||
)?;
|
||||
let mut parser = self.make_parser(&source)?;
|
||||
let module = Ref::new(parser.parse()?);
|
||||
limits::enforce_memory_limit().map_err(|err| anyhow!(err))?;
|
||||
@@ -232,7 +279,11 @@ impl Engine {
|
||||
#[cfg(feature = "std")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
|
||||
pub fn add_policy_from_file<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<String> {
|
||||
let source = Source::from_file(path)?;
|
||||
let source = Source::from_file_with_limits(
|
||||
path,
|
||||
self.policy_length_config.max_file_bytes,
|
||||
self.policy_length_config.max_lines,
|
||||
)?;
|
||||
let mut parser = self.make_parser(&source)?;
|
||||
let module = Ref::new(parser.parse()?);
|
||||
limits::enforce_memory_limit().map_err(|err| anyhow!(err))?;
|
||||
@@ -918,15 +969,24 @@ impl Engine {
|
||||
|
||||
fn make_query(&mut self, query: String) -> Result<(NodeRef<Module>, NodeRef<Query>, Schedule)> {
|
||||
let mut query_module = {
|
||||
let source = Source::from_contents(
|
||||
let source = Source::from_contents_with_limits(
|
||||
"<query_module.rego>".to_owned(),
|
||||
"package __internal_query_module".to_owned(),
|
||||
self.policy_length_config.max_file_bytes,
|
||||
self.policy_length_config.max_lines,
|
||||
)?;
|
||||
Parser::new(&source)?.parse()?
|
||||
let mut parser = Parser::new(&source)?;
|
||||
parser.set_max_col(self.policy_length_config.max_col);
|
||||
parser.parse()?
|
||||
};
|
||||
|
||||
// Parse the query.
|
||||
let query_source = Source::from_contents("<query.rego>".to_string(), query)?;
|
||||
let query_source = Source::from_contents_with_limits(
|
||||
"<query.rego>".to_string(),
|
||||
query,
|
||||
self.policy_length_config.max_file_bytes,
|
||||
self.policy_length_config.max_lines,
|
||||
)?;
|
||||
let mut parser = self.make_parser(&query_source)?;
|
||||
let query_node = parser.parse_user_query()?;
|
||||
query_module.num_expressions = parser.num_expressions();
|
||||
@@ -1506,6 +1566,7 @@ impl Engine {
|
||||
|
||||
fn make_parser<'a>(&self, source: &'a Source) -> Result<Parser<'a>> {
|
||||
let mut parser = Parser::new(source)?;
|
||||
parser.set_max_col(self.policy_length_config.max_col);
|
||||
if self.rego_v1 {
|
||||
parser.enable_rego_v1()?;
|
||||
}
|
||||
@@ -1524,6 +1585,7 @@ impl Engine {
|
||||
rego_v1: true, // Value doesn't matter since this is used only for policy parsing
|
||||
prepared: true,
|
||||
execution_timer_config: None,
|
||||
policy_length_config: PolicyLengthConfig::default(), // Compiled policies are already parsed, so these length limits are not used
|
||||
};
|
||||
engine.apply_effective_execution_timer_config();
|
||||
engine
|
||||
|
||||
71
src/lexer.rs
71
src/lexer.rs
@@ -1,18 +1,21 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use crate::utils::limits::{DEFAULT_MAX_COL, DEFAULT_MAX_FILE_BYTES, DEFAULT_MAX_LINES};
|
||||
// SAFETY: Arithmetic operations in this module are safe by design:
|
||||
// 1. MAX_COL=1024 prevents column counter overflow (enforced by advance_col)
|
||||
// 2. File size is capped by MAX_FILE_BYTES at load time
|
||||
// 3. Total line count is capped by MAX_LINES at load time
|
||||
// 1. Column width is bounded by a configurable max_col limit (default DEFAULT_MAX_COL=1024,
|
||||
// overridable via Engine::set_policy_length_config) and enforced by advance_col
|
||||
// 2. File size is bounded by a configurable limit (default DEFAULT_MAX_FILE_BYTES) at load time
|
||||
// 3. Total line count is bounded by a configurable limit (default DEFAULT_MAX_LINES) at load time
|
||||
// 4. State-modifying operations (advance_col/advance_line) use checked arithmetic
|
||||
// 5. Remaining arithmetic is for bounded calculations (spans, error reporting)
|
||||
// where operands are constrained by MAX_COL and file size/line limits
|
||||
// where operands are constrained by the column width and file size/line limits
|
||||
// 6. Defensive saturating_sub used for subtractions that could theoretically underflow
|
||||
use crate::*;
|
||||
use core::cmp;
|
||||
use core::fmt::{self, Debug, Formatter};
|
||||
use core::iter::Peekable;
|
||||
use core::num::{NonZeroU32, NonZeroUsize};
|
||||
use core::ops::Range;
|
||||
use core::str::CharIndices;
|
||||
|
||||
@@ -25,14 +28,6 @@ fn check_memory_limit() -> Result<()> {
|
||||
crate::utils::limits::check_memory_limit_if_needed().map_err(|err| anyhow!(err))
|
||||
}
|
||||
|
||||
// Maximum column width to prevent overflow and catch pathological input.
|
||||
// Lines exceeding this are likely minified/generated code or attack attempts.
|
||||
const MAX_COL: u32 = 1024;
|
||||
// Maximum allowed policy file size in bytes (1 MiB) to reject pathological inputs early.
|
||||
const MAX_FILE_BYTES: usize = 1_048_576;
|
||||
// Maximum allowed number of lines to avoid pathological or minified inputs.
|
||||
const MAX_LINES: usize = 20_000;
|
||||
|
||||
#[inline]
|
||||
fn usize_to_u32(value: usize) -> Result<u32> {
|
||||
u32::try_from(value).map_err(|_| anyhow!("value exceeds u32::MAX"))
|
||||
@@ -172,8 +167,17 @@ impl cmp::Ord for SourceStr {
|
||||
|
||||
impl Source {
|
||||
pub fn from_contents(file: String, contents: String) -> Result<Source> {
|
||||
if contents.len() > MAX_FILE_BYTES {
|
||||
bail!("{file} exceeds maximum allowed policy file size {MAX_FILE_BYTES} bytes");
|
||||
Self::from_contents_with_limits(file, contents, DEFAULT_MAX_FILE_BYTES, DEFAULT_MAX_LINES)
|
||||
}
|
||||
|
||||
pub fn from_contents_with_limits(
|
||||
file: String,
|
||||
contents: String,
|
||||
max_file_bytes: NonZeroUsize,
|
||||
max_lines: NonZeroUsize,
|
||||
) -> Result<Source> {
|
||||
if contents.len() > max_file_bytes.get() {
|
||||
bail!("{file} exceeds maximum allowed policy file size {max_file_bytes} bytes",);
|
||||
}
|
||||
let mut lines = vec![];
|
||||
let mut prev_ch = ' ';
|
||||
@@ -186,8 +190,8 @@ impl Source {
|
||||
'\r' => prev_pos,
|
||||
_ => i_u32,
|
||||
};
|
||||
if lines.len() >= MAX_LINES {
|
||||
bail!("{file} exceeds maximum allowed line count {MAX_LINES}");
|
||||
if lines.len() >= max_lines.get() {
|
||||
bail!("{file} exceeds maximum allowed line count {max_lines}",);
|
||||
}
|
||||
lines.push((start, end));
|
||||
// Enforce the current global memory cap after recording each line span.
|
||||
@@ -200,8 +204,8 @@ impl Source {
|
||||
|
||||
let start_usize = usize::try_from(start).unwrap_or(usize::MAX);
|
||||
if start_usize < contents.len() {
|
||||
if lines.len() >= MAX_LINES {
|
||||
bail!("{file} exceeds maximum allowed line count {MAX_LINES}");
|
||||
if lines.len() >= max_lines.get() {
|
||||
bail!("{file} exceeds maximum allowed line count {max_lines}",);
|
||||
}
|
||||
lines.push((start, usize_to_u32(contents.len())?));
|
||||
// Enforce the global limit after appending the final line span.
|
||||
@@ -212,8 +216,8 @@ impl Source {
|
||||
check_memory_limit()?;
|
||||
} else {
|
||||
let s = usize_to_u32(contents.len().saturating_sub(1))?;
|
||||
if lines.len() >= MAX_LINES {
|
||||
bail!("{file} exceeds maximum allowed line count {MAX_LINES}");
|
||||
if lines.len() >= max_lines.get() {
|
||||
bail!("{file} exceeds maximum allowed line count {max_lines}",);
|
||||
}
|
||||
lines.push((s, s));
|
||||
// Enforce the global limit after storing the trailing span.
|
||||
@@ -230,12 +234,26 @@ impl Source {
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Source> {
|
||||
Self::from_file_with_limits(path, DEFAULT_MAX_FILE_BYTES, DEFAULT_MAX_LINES)
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub fn from_file_with_limits<P: AsRef<std::path::Path>>(
|
||||
path: P,
|
||||
max_file_bytes: NonZeroUsize,
|
||||
max_lines: NonZeroUsize,
|
||||
) -> Result<Source> {
|
||||
let contents = match std::fs::read_to_string(&path) {
|
||||
Ok(c) => c,
|
||||
Err(e) => bail!("Failed to read {}. {e}", path.as_ref().display()),
|
||||
};
|
||||
// TODO: retain path instead of converting to string
|
||||
Self::from_contents(path.as_ref().to_string_lossy().to_string(), contents)
|
||||
Self::from_contents_with_limits(
|
||||
path.as_ref().to_string_lossy().to_string(),
|
||||
contents,
|
||||
max_file_bytes,
|
||||
max_lines,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn file(&self) -> &String {
|
||||
@@ -370,6 +388,7 @@ pub struct Lexer<'source> {
|
||||
iter: Peekable<CharIndices<'source>>,
|
||||
line: u32,
|
||||
col: u32,
|
||||
max_col: NonZeroU32,
|
||||
unknown_char_is_symbol: bool,
|
||||
allow_slash_star_escape: bool,
|
||||
comment_starts_with_double_slash: bool,
|
||||
@@ -393,6 +412,7 @@ impl<'source> Lexer<'source> {
|
||||
iter: source.contents().char_indices().peekable(),
|
||||
line: 1,
|
||||
col: 1,
|
||||
max_col: DEFAULT_MAX_COL,
|
||||
unknown_char_is_symbol: false,
|
||||
allow_slash_star_escape: false,
|
||||
comment_starts_with_double_slash: false,
|
||||
@@ -420,6 +440,10 @@ impl<'source> Lexer<'source> {
|
||||
self.double_colon_token = b;
|
||||
}
|
||||
|
||||
pub const fn set_max_col(&mut self, max_col: NonZeroU32) {
|
||||
self.max_col = max_col;
|
||||
}
|
||||
|
||||
#[cfg(feature = "azure-rbac")]
|
||||
pub const fn set_enable_rbac_tokens(&mut self, b: bool) {
|
||||
self.enable_rbac_tokens = b;
|
||||
@@ -439,15 +463,16 @@ impl<'source> Lexer<'source> {
|
||||
|
||||
#[inline]
|
||||
fn advance_col(&mut self, delta: u32) -> Result<()> {
|
||||
let max_col = self.max_col.get();
|
||||
let new_col = self
|
||||
.col
|
||||
.checked_add(delta)
|
||||
.filter(|&c| c <= MAX_COL)
|
||||
.filter(|&c| c <= max_col)
|
||||
.ok_or_else(|| {
|
||||
self.source.error(
|
||||
self.line,
|
||||
self.col,
|
||||
&format!("line exceeds maximum column width of {MAX_COL}"),
|
||||
&format!("line exceeds maximum column width of {max_col}"),
|
||||
)
|
||||
})?;
|
||||
self.col = new_col;
|
||||
|
||||
@@ -167,6 +167,7 @@ pub use engine::Engine;
|
||||
pub use lexer::Source;
|
||||
pub use policy_info::PolicyInfo;
|
||||
pub use utils::limits::LimitError;
|
||||
pub use utils::limits::PolicyLengthConfig;
|
||||
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
|
||||
pub use utils::limits::{
|
||||
check_global_memory_limit, enforce_memory_limit, flush_thread_memory_counters,
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::value::*;
|
||||
use crate::*;
|
||||
|
||||
use alloc::collections::BTreeMap;
|
||||
use core::num::NonZeroU32;
|
||||
use core::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
@@ -198,6 +199,10 @@ impl<'source> Parser<'source> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_max_col(&mut self, max_col: NonZeroU32) {
|
||||
self.lexer.set_max_col(max_col);
|
||||
}
|
||||
|
||||
pub fn get_path_ref_components_into(refr: &Ref<Expr>, comps: &mut Vec<Span>) -> Result<()> {
|
||||
match refr.as_ref() {
|
||||
Expr::RefDot { refr, field, .. } => {
|
||||
|
||||
36
src/utils/limits/length.rs
Normal file
36
src/utils/limits/length.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use core::num::{NonZeroU32, NonZeroUsize};
|
||||
|
||||
/// Policy source length limits enforced when loading policy files.
|
||||
///
|
||||
/// These limits reject pathological or generated inputs early, before parsing begins.
|
||||
/// Use [`Default::default`] for the built-in thresholds.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct PolicyLengthConfig {
|
||||
/// Maximum column width per line (default: 1024).
|
||||
pub max_col: NonZeroU32,
|
||||
/// Maximum policy file size in bytes (default: 1 MiB).
|
||||
pub max_file_bytes: NonZeroUsize,
|
||||
/// Maximum number of lines per policy file (default: 20 000).
|
||||
pub max_lines: NonZeroUsize,
|
||||
}
|
||||
|
||||
// Maximum column width to prevent overflow and catch pathological input.
|
||||
// Lines exceeding this are likely minified/generated code or attack attempts.
|
||||
pub const DEFAULT_MAX_COL: NonZeroU32 = NonZeroU32::new(1024).unwrap();
|
||||
// Maximum allowed policy file size in bytes (1 MiB) to reject pathological inputs early.
|
||||
pub const DEFAULT_MAX_FILE_BYTES: NonZeroUsize = NonZeroUsize::new(1_048_576).unwrap();
|
||||
// Maximum allowed number of lines to avoid pathological or minified inputs.
|
||||
pub const DEFAULT_MAX_LINES: NonZeroUsize = NonZeroUsize::new(20_000).unwrap();
|
||||
|
||||
impl Default for PolicyLengthConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_col: DEFAULT_MAX_COL,
|
||||
max_file_bytes: DEFAULT_MAX_FILE_BYTES,
|
||||
max_lines: DEFAULT_MAX_LINES,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
mod error;
|
||||
mod length;
|
||||
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
|
||||
mod memory;
|
||||
mod time;
|
||||
@@ -27,6 +28,9 @@ pub use time::{
|
||||
ExecutionTimer, ExecutionTimerConfig, TimeSource,
|
||||
};
|
||||
|
||||
pub use length::PolicyLengthConfig;
|
||||
pub(crate) use length::{DEFAULT_MAX_COL, DEFAULT_MAX_FILE_BYTES, DEFAULT_MAX_LINES};
|
||||
|
||||
#[cfg(test)]
|
||||
pub use time::acquire_limits_test_lock;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user