@@ -32,8 +32,10 @@ pub(super) const MAX_EXEC_ARG_LEN: usize = 32 * 1024; // 32 KiB
3232/// Maximum length of the workdir field (bytes).
3333pub ( super ) const MAX_EXEC_WORKDIR_LEN : usize = 4096 ;
3434
35- /// Validate fields of an `ExecSandboxRequest` for control characters and size
36- /// limits before constructing a shell command string.
35+ /// Validate exec request size limits and field-specific character constraints.
36+ ///
37+ /// Command arguments only reject NUL (newlines are valid for inline scripts).
38+ /// Environment values and workdir reject both NUL and newlines.
3739pub ( super ) fn validate_exec_request_fields ( req : & ExecSandboxRequest ) -> Result < ( ) , Status > {
3840 if req. command . len ( ) > MAX_EXEC_COMMAND_ARGS {
3941 return Err ( Status :: invalid_argument ( format ! (
@@ -46,7 +48,7 @@ pub(super) fn validate_exec_request_fields(req: &ExecSandboxRequest) -> Result<(
4648 "command argument {i} exceeds {MAX_EXEC_ARG_LEN} byte limit"
4749 ) ) ) ;
4850 }
49- reject_control_chars ( arg, & format ! ( "command argument {i}" ) ) ?;
51+ reject_null_char ( arg, & format ! ( "command argument {i}" ) ) ?;
5052 }
5153 for ( key, value) in & req. environment {
5254 if value. len ( ) > MAX_EXEC_ARG_LEN {
@@ -70,11 +72,23 @@ pub(super) fn validate_exec_request_fields(req: &ExecSandboxRequest) -> Result<(
7072
7173/// Reject null bytes and newlines in a user-supplied value.
7274pub ( super ) fn reject_control_chars ( value : & str , field_name : & str ) -> Result < ( ) , Status > {
75+ reject_null_char ( value, field_name) ?;
76+ reject_newline_chars ( value, field_name) ?;
77+ Ok ( ( ) )
78+ }
79+
80+ /// Reject null bytes in a user-supplied value.
81+ pub ( super ) fn reject_null_char ( value : & str , field_name : & str ) -> Result < ( ) , Status > {
7382 if value. bytes ( ) . any ( |b| b == 0 ) {
7483 return Err ( Status :: invalid_argument ( format ! (
7584 "{field_name} contains null bytes"
7685 ) ) ) ;
7786 }
87+ Ok ( ( ) )
88+ }
89+
90+ /// Reject newline and carriage return characters in a user-supplied value.
91+ pub ( super ) fn reject_newline_chars ( value : & str , field_name : & str ) -> Result < ( ) , Status > {
7892 if value. bytes ( ) . any ( |b| b == b'\n' || b == b'\r' ) {
7993 return Err ( Status :: invalid_argument ( format ! (
8094 "{field_name} contains newline or carriage return characters"
@@ -1800,4 +1814,54 @@ mod tests {
18001814 assert ! ( reject_control_chars( "line1\n line2" , "test" ) . is_err( ) ) ;
18011815 assert ! ( reject_control_chars( "line1\r line2" , "test" ) . is_err( ) ) ;
18021816 }
1817+
1818+ #[ test]
1819+ fn validate_exec_allows_newlines_in_command_args ( ) {
1820+ let req = ExecSandboxRequest {
1821+ sandbox_id : "test" . to_string ( ) ,
1822+ command : vec ! [
1823+ "python3" . to_string( ) ,
1824+ "-c" . to_string( ) ,
1825+ "def f():\n return 1\n print(f())" . to_string( ) ,
1826+ ] ,
1827+ ..Default :: default ( )
1828+ } ;
1829+ assert ! ( validate_exec_request_fields( & req) . is_ok( ) ) ;
1830+ }
1831+
1832+ #[ test]
1833+ fn validate_exec_still_rejects_null_bytes_in_command_args ( ) {
1834+ let req = ExecSandboxRequest {
1835+ sandbox_id : "test" . to_string ( ) ,
1836+ command : vec ! [ "echo" . to_string( ) , "hello\x00 world" . to_string( ) ] ,
1837+ ..Default :: default ( )
1838+ } ;
1839+ let err = validate_exec_request_fields ( & req) . unwrap_err ( ) ;
1840+ assert ! ( err. message( ) . contains( "null" ) ) ;
1841+ }
1842+
1843+ #[ test]
1844+ fn validate_exec_still_rejects_newlines_in_workdir ( ) {
1845+ let req = ExecSandboxRequest {
1846+ sandbox_id : "test" . to_string ( ) ,
1847+ command : vec ! [ "ls" . to_string( ) ] ,
1848+ workdir : "/tmp\n malicious" . to_string ( ) ,
1849+ ..Default :: default ( )
1850+ } ;
1851+ let err = validate_exec_request_fields ( & req) . unwrap_err ( ) ;
1852+ assert ! ( err. message( ) . contains( "newline" ) ) ;
1853+ }
1854+
1855+ #[ test]
1856+ fn validate_exec_still_rejects_newlines_in_env_values ( ) {
1857+ let req = ExecSandboxRequest {
1858+ sandbox_id : "test" . to_string ( ) ,
1859+ command : vec ! [ "ls" . to_string( ) ] ,
1860+ environment : std:: iter:: once ( ( "VAR" . to_string ( ) , "val\n malicious" . to_string ( ) ) )
1861+ . collect ( ) ,
1862+ ..Default :: default ( )
1863+ } ;
1864+ let err = validate_exec_request_fields ( & req) . unwrap_err ( ) ;
1865+ assert ! ( err. message( ) . contains( "newline" ) ) ;
1866+ }
18031867}
0 commit comments