├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── INSTALL.md ├── LICENSE ├── README.md ├── mkbind.sh └── src ├── lib.rs ├── main.rs ├── mytcl.h └── rstcl.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [root] 2 | name = "tclscan" 3 | version = "0.0.1" 4 | dependencies = [ 5 | "bindgen 0.14.0 (git+https://github.com/crabtw/rust-bindgen.git)", 6 | "docopt 0.6.64 (registry+https://github.com/rust-lang/crates.io-index)", 7 | "docopt_macros 0.6.64 (registry+https://github.com/rust-lang/crates.io-index)", 8 | "enum_primitive 0.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 9 | "num 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", 10 | "rustc-serialize 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", 11 | ] 12 | 13 | [[package]] 14 | name = "bindgen" 15 | version = "0.14.0" 16 | source = "git+https://github.com/crabtw/rust-bindgen.git#e8d1248983aec64177d2d015cbd88f70f1ee1f82" 17 | dependencies = [ 18 | "libc 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 19 | "log 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 20 | "syntex_syntax 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 21 | ] 22 | 23 | [[package]] 24 | name = "bitflags" 25 | version = "0.1.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | 28 | [[package]] 29 | name = "docopt" 30 | version = "0.6.64" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | dependencies = [ 33 | "regex 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)", 34 | "rustc-serialize 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", 35 | "strsim 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 36 | ] 37 | 38 | [[package]] 39 | name = "docopt_macros" 40 | version = "0.6.64" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | dependencies = [ 43 | "docopt 0.6.64 (registry+https://github.com/rust-lang/crates.io-index)", 44 | ] 45 | 46 | [[package]] 47 | name = "enum_primitive" 48 | version = "0.0.2" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | dependencies = [ 51 | "num 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", 52 | ] 53 | 54 | [[package]] 55 | name = "kernel32-sys" 56 | version = "0.1.0" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | dependencies = [ 59 | "winapi 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", 60 | ] 61 | 62 | [[package]] 63 | name = "libc" 64 | version = "0.1.6" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | 67 | [[package]] 68 | name = "log" 69 | version = "0.3.1" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | dependencies = [ 72 | "libc 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 73 | ] 74 | 75 | [[package]] 76 | name = "num" 77 | version = "0.1.24" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | dependencies = [ 80 | "rand 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", 81 | "rustc-serialize 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", 82 | ] 83 | 84 | [[package]] 85 | name = "rand" 86 | version = "0.3.8" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | dependencies = [ 89 | "libc 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 90 | ] 91 | 92 | [[package]] 93 | name = "regex" 94 | version = "0.1.30" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | 97 | [[package]] 98 | name = "rustc-serialize" 99 | version = "0.3.14" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | 102 | [[package]] 103 | name = "strsim" 104 | version = "0.3.0" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | 107 | [[package]] 108 | name = "syntex_fmt_macros" 109 | version = "0.4.2" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | dependencies = [ 112 | "unicode-xid 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 113 | ] 114 | 115 | [[package]] 116 | name = "syntex_syntax" 117 | version = "0.4.2" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | dependencies = [ 120 | "bitflags 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 121 | "libc 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 122 | "log 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 123 | "rustc-serialize 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", 124 | "syntex_fmt_macros 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 125 | "term 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", 126 | "unicode-xid 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 127 | ] 128 | 129 | [[package]] 130 | name = "term" 131 | version = "0.2.7" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | dependencies = [ 134 | "kernel32-sys 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 135 | "winapi 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", 136 | ] 137 | 138 | [[package]] 139 | name = "unicode-xid" 140 | version = "0.0.1" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | 143 | [[package]] 144 | name = "winapi" 145 | version = "0.1.17" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | dependencies = [ 148 | "libc 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 149 | ] 150 | 151 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "tclscan" 4 | version = "0.0.1" 5 | authors = ["Aidan Hobson Sayers "] 6 | 7 | [dependencies] 8 | docopt = "1" 9 | enum_primitive = "0.0.2" 10 | num = "0.1.24" 11 | rustc-serialize = "0.3.14" 12 | 13 | [dependencies.bindgen] 14 | git = "https://github.com/rust-lang-nursery/rust-bindgen.git" 15 | tag = "v0.43.1" 16 | 17 | [[bin]] 18 | 19 | name = "tclscan" 20 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | This installation manual describes how to install tclscan based on your running tcl version. 3 | 4 | ## Introduction 5 | To complete the instructions experience with a Linux environment rudimentary udnerstanding of coding and some patients are required. 6 | 7 | ### Environment debian / ubuntu 8 | Install `tcl-dev` and `clang`. 9 | ```bash 10 | sudo apt-get install tcl-dev clang 11 | ``` 12 | ### Environment redhat 13 | ```bash 14 | sudo subscription-manager repos --enable rhel-7-server-devtools-rpms 15 | sudo subscription-manager repos --enable rhel-server-rhscl-7-rpms 16 | sudo yum install llvm-toolset-7 tcl-devel 17 | ``` 18 | For more troubleshooting see https://developers.redhat.com/products/clang-llvm-go-rust/hello-world/#fndtn-windows 19 | 20 | ### Installing rust 21 | 22 | Install rustup and cargo from source. 23 | ```bash 24 | curl https://sh.rustup.rs -sSf | sh 25 | ``` 26 | Some of the code relies on features in the nightly build of rust, so you need to switch to it. 27 | 28 | ```bash 29 | $ rustup toolchain install nightly 30 | info: syncing channel updates for 'nightly-x86_64-unknown-linux-gnu' 31 | $ rustup default nightly 32 | ``` 33 | 34 | Activate the rust environment in your shell 35 | 36 | ```bash 37 | source ~/.cargo/env 38 | ``` 39 | 40 | ### Installing rust-bindgen 41 | Rust-bindgen is a tool that allows you to generate rust bindings from c header files. 42 | 43 | Clone the `rust-bindgen` repository. 44 | ```bash 45 | git clone https://github.com/rust-lang-nursery/rust-bindgen 46 | ``` 47 | 48 | Check out a recent release 49 | ``` 50 | git tag 51 | 52 | ... 53 | v0.42.2 54 | v0.42.3 55 | v0.43.0 56 | v0.43.1 57 | vX.YY.Z 58 | 59 | git checkout vX.YY.Z (e.g. the latest branch) 60 | ``` 61 | 62 | Update the cargo and build 63 | ```bash 64 | cargo update 65 | cargo build 66 | ``` 67 | 68 | ### Creating a build rust tcl header 69 | Prepare the rust to c bindings in two steps: 70 | 1. Edit `tclscan/src/mytcl.h` to define the path to `tcl.h` installed through `tcl-dev`/`tcl-devel`. 71 | 2. Locate youre `libclang.so` file that you installed from the clang package. 72 | 73 | Generate `tcl.rs` in the `tclsca/src/` directory using `bindgen` 74 | ```bash 75 | LD_PRELOAD=/usr/lib/llvm-6.0/lib/libclang.so.1 rust-bindgen/target/debug/bindgen -o tclscan/src/tcl.rs tclscan/src/mytcl.h 76 | ``` 77 | 78 | If you get the error 79 | 80 | ```bash 81 | error: 'rustfmt' is not installed for the toolchain 'nightly-x86_64-unknown-linux-gnu' 82 | ``` 83 | 84 | Simply install rustfmt: 85 | 86 | ```bash 87 | $ rustup component add rustfmt --toolchain nightly-x86_64-unknown-linux-gnu 88 | ``` 89 | 90 | Upate your running environment 91 | ```bash 92 | cargo update 93 | ``` 94 | 95 | Set environment variable for linking against libtcl.so 96 | ```bash 97 | export RUSTFLAGS="-C link_args="-ltcl"" 98 | ``` 99 | 100 | Compile the program 101 | ```bash 102 | cargo build 103 | ``` 104 | 105 | A successfull build produce the executable `tclscan/target/debug/tclscan`. 106 | If you want to install tclscan on other systems, keep in mind that it depends on `libtcl.so` in your `$LD_LIBRARY_PATH`. 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Aidan Hobson Sayers and Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tclscan 2 | 3 | tclscan is a tool to scan tcl code for unexpected/unsafe expressions that may have 4 | undesirable effects like double evaluation. 5 | 6 | See [the documentation for check_command](https://github.com/aidanhs/tclscan/blob/master/src/lib.rs#L117-L143) 7 | for examples of usage and results. 8 | 9 | See [INSTALL.md](INSTALL.md) for details on how to build/install. 10 | -------------------------------------------------------------------------------- /mkbind.sh: -------------------------------------------------------------------------------- 1 | cd ~/rust/rust-bindgen/target/debug 2 | LD_PRELOAD=/usr/lib/llvm-3.4/lib/libclang.so ./bindgen -ltcl -builtins \ 3 | -o $(cd -)/src/tcl.rs $(cd -)/src/mytcl.h 4 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(core)] 2 | //#![feature(collections)] 3 | #![feature(libc)] 4 | #![feature(slice_patterns)] 5 | //#![feature(str_char)] 6 | #![feature(rustc_private)] 7 | 8 | extern crate libc; 9 | // https://github.com/rust-lang/rust/issues/16920 10 | #[macro_use] extern crate enum_primitive; 11 | extern crate num; 12 | 13 | use std::iter; 14 | use std::fmt; 15 | use self::CheckResult::*; // TODO: why does swapping this line with one below break? 16 | use rstcl::TokenType; 17 | 18 | pub mod rstcl; 19 | #[allow(dead_code, non_upper_case_globals, non_camel_case_types, non_snake_case, raw_pointer_derive)] 20 | mod tcl; 21 | 22 | // http://www.tcl.tk/doc/howto/stubs.html 23 | // Ideally would use stubs but they seem to not work 24 | 25 | // When https://github.com/crabtw/rust-bindgen/issues/89 is fixed 26 | //#![feature(phase)] 27 | //#[phase(plugin)] extern crate bindgen; 28 | // 29 | //#[allow(dead_code, uppercase_variables, non_camel_case_types)] 30 | //mod tcl_bindings { 31 | // bindgen!("./mytcl.h", match="tcl.h", link="tclstub") 32 | //} 33 | 34 | #[derive(PartialEq)] 35 | pub enum CheckResult<'a> { 36 | // context, message, problem code 37 | Warn(&'a str, &'static str, &'a str), 38 | Danger(&'a str, &'static str, &'a str), 39 | } 40 | impl<'b> fmt::Display for CheckResult<'b> { 41 | fn fmt<'a>(&'a self, f: &mut fmt::Formatter) -> fmt::Result { 42 | return match self { 43 | &Warn(ctx, msg, line) => write!(f, "WARN: {} at `{}` in `{}`", msg, line, ctx), 44 | &Danger(ctx, msg, line) => write!(f, "DANGER: {} at `{}` in `{}`", msg, line, ctx), 45 | }; 46 | } 47 | } 48 | 49 | #[derive(Clone)] 50 | enum Code { 51 | Block, 52 | Expr, 53 | Literal, 54 | Normal, 55 | } 56 | 57 | fn check_literal<'a, 'b>(ctx: &'a str, token: &'b rstcl::TclToken<'a>) -> Vec> { 58 | let token_str = token.val; 59 | assert!(token_str.len() > 0); 60 | return if token_str.chars().nth(0) == Some('{') { 61 | vec![] 62 | } else if token_str.contains('$') { 63 | vec![Danger(ctx, "Expected literal, found $", token_str)] 64 | } else if token_str.contains('[') { 65 | vec![Danger(ctx, "Expected literal, found [", token_str)] 66 | } else { 67 | vec![] 68 | } 69 | } 70 | 71 | // Does this variable only contain safe characters? 72 | // Only used by is_safe_val 73 | fn is_safe_var(token: &rstcl::TclToken) -> bool { 74 | assert!(token.ttype == TokenType::Variable); 75 | return false 76 | } 77 | 78 | // Does the return value of this function only contain safe characters? 79 | // Only used by is_safe_val. 80 | fn is_safe_cmd(token: &rstcl::TclToken) -> bool { 81 | let string = token.val; 82 | assert!(string.starts_with("[") && string.ends_with("]")); 83 | let script = &string[1..string.len()-1]; 84 | let parses = rstcl::parse_script(script); 85 | // Empty script 86 | if parses.len() == 0 { 87 | return true; 88 | } 89 | let token_strs: Vec<&str> = parses[0].tokens.iter().map(|e| e.val).collect(); 90 | return match &token_strs[..] { 91 | ["llength", _] | 92 | ["clock", "seconds"] | 93 | ["info", "exists", ..] | 94 | ["catch", ..] => true, 95 | _ => false, 96 | }; 97 | } 98 | 99 | // Check whether a value can ever cause or assist in any security flaw i.e. 100 | // whether it may contain special characters. 101 | // We do *not* concern ourselves with vulnerabilities in sub-commands. That 102 | // should happen elsewhere. 103 | fn is_safe_val(token: &rstcl::TclToken) -> bool { 104 | assert!(token.val.len() > 0); 105 | for tok in token.iter() { 106 | let is_safe = match tok.ttype { 107 | TokenType::Variable => is_safe_var(tok), 108 | TokenType::Command => is_safe_cmd(tok), 109 | _ => true, 110 | }; 111 | if !is_safe { 112 | return false; 113 | } 114 | } 115 | return true; 116 | } 117 | 118 | /// Checks if a parsed command is insecure 119 | /// 120 | /// ``` 121 | /// use tclscan::rstcl::parse_command; 122 | /// use tclscan::check_command; 123 | /// use tclscan::CheckResult; 124 | /// use tclscan::CheckResult::{Danger,Warn}; 125 | /// fn c<'a>(string: &'a str) -> Vec> { 126 | /// return check_command(string, &parse_command(string).0.tokens); 127 | /// } 128 | /// assert!(c(("puts x")) == vec![]); 129 | /// assert!(c(("puts [x]")) == vec![]); 130 | /// assert!(c(("puts [x\n ]")) == vec![]); 131 | /// assert!(c(("puts [x;y]")) == vec![]); 132 | /// assert!(c(("puts [x;eval $y]")) == vec![Danger("eval $y", "Dangerous unquoted block", "$y")]); 133 | /// assert!(c(("puts [;;eval $y]")) == vec![Danger("eval $y", "Dangerous unquoted block", "$y")]); 134 | /// assert!(c(("puts [eval $x]")) == vec![Danger("eval $x", "Dangerous unquoted block", "$x")]); 135 | /// assert!(c(("expr {[blah]}")) == vec![]); 136 | /// assert!(c(("expr \"[blah]\"")) == vec![Danger("expr \"[blah]\"", "Dangerous unquoted expr", "\"[blah]\"")]); 137 | /// assert!(c(("expr {\\\n0}")) == vec![]); 138 | /// assert!(c(("expr {[expr \"[blah]\"]}")) == vec![Danger("expr \"[blah]\"", "Dangerous unquoted expr", "\"[blah]\"")]); 139 | /// assert!(c(("if [info exists abc] {}")) == vec![Warn("if [info exists abc] {}", "Unquoted expr", "[info exists abc]")]); 140 | /// assert!(c(("if [abc] {}")) == vec![Danger("if [abc] {}", "Dangerous unquoted expr", "[abc]")]); 141 | /// assert!(c(("a${x} blah")) == vec![Warn("a${x} blah", "Non-literal command, cannot scan", "a${x}")]); 142 | /// assert!(c(("set a []")) == vec![]); 143 | /// ``` 144 | pub fn check_command<'a, 'b>(ctx: &'a str, tokens: &'b Vec>) -> Vec> { 145 | let mut results = vec![]; 146 | // First check all subcommands which will be substituted 147 | for tok in tokens.iter() { 148 | for subtok in tok.iter().filter(|tok| tok.ttype == TokenType::Command) { 149 | results.extend(scan_command(subtok.val).into_iter()); 150 | } 151 | } 152 | // The empty command (caused by e.g. `[]`, `;;`, last parse in a script) 153 | if tokens.len() == 0 { 154 | return results; 155 | } 156 | // Now check if the command name itself isn't a literal 157 | if check_literal(ctx, &tokens[0]).into_iter().len() > 0 { 158 | results.push(Warn(ctx, "Non-literal command, cannot scan", tokens[0].val)); 159 | return results; 160 | } 161 | // Now check the command-specific interpretation of arguments etc 162 | let param_types = match tokens[0].val { 163 | // eval script 164 | "eval" => iter::repeat(Code::Block).take(tokens.len()-1).collect(), 165 | // catch script [result]? [options]? 166 | "catch" => { 167 | let mut param_types = vec![Code::Block]; 168 | if tokens.len() == 3 || tokens.len() == 4 { 169 | let new_params: Vec = iter::repeat(Code::Literal).take(tokens.len()-2).collect(); 170 | param_types.extend_from_slice(&new_params); 171 | } 172 | param_types 173 | } 174 | // expr [arg]+ 175 | "expr" => tokens[1..].iter().map(|_| Code::Expr).collect(), 176 | // proc name args body 177 | "proc" => vec![Code::Literal, Code::Literal, Code::Block], 178 | // for init cond iter body 179 | "for" => vec![Code::Block, Code::Expr, Code::Block, Code::Block], 180 | // foreach [varname list]+ body 181 | "foreach" => vec![Code::Literal, Code::Normal, Code::Block], 182 | // while cond body 183 | "while" => vec![Code::Expr, Code::Block], 184 | // if cond body [elseif cond body]* [else body]? 185 | "if" => { 186 | let mut param_types = vec![Code::Expr, Code::Block]; 187 | let mut i = 3; 188 | while i < tokens.len() { 189 | param_types.extend_from_slice(&match tokens[i].val { 190 | "elseif" => vec![Code::Literal, Code::Expr, Code::Block], 191 | "else" => vec![Code::Literal, Code::Block], 192 | _ => { break; }, 193 | }); 194 | i = param_types.len() + 1; 195 | } 196 | param_types 197 | }, 198 | _ => iter::repeat(Code::Normal).take(tokens.len()-1).collect(), 199 | }; 200 | if param_types.len() != tokens.len() - 1 { 201 | results.push(Warn(ctx, "badly formed command", tokens[0].val)); 202 | return results; 203 | } 204 | for (param_type, param) in param_types.iter().zip(tokens[1..].iter()) { 205 | let check_results: Vec> = match *param_type { 206 | Code::Block => check_block(ctx, param), 207 | Code::Expr => check_expr(ctx, param), 208 | Code::Literal => check_literal(ctx, param), 209 | Code::Normal => vec![], 210 | }; 211 | results.extend(check_results.into_iter()); 212 | } 213 | return results; 214 | } 215 | 216 | /// Scans a block (i.e. should be quoted) for danger 217 | fn check_block<'a, 'b>(ctx: &'a str, token: &'b rstcl::TclToken<'a>) -> Vec> { 218 | let block_str = token.val; 219 | if !(block_str.starts_with("{") && block_str.ends_with("}")) { 220 | return vec!(match is_safe_val(token) { 221 | true => Warn(ctx, "Unquoted block", block_str), 222 | false => Danger(ctx, "Dangerous unquoted block", block_str), 223 | }); 224 | } 225 | // Block isn't inherently dangerous, let's check functions inside the block 226 | let script_str = &block_str[1..block_str.len()-1]; 227 | return scan_script(script_str); 228 | } 229 | 230 | /// Scans an expr (i.e. should be quoted) for danger 231 | fn check_expr<'a, 'b>(ctx: &'a str, token: &'b rstcl::TclToken<'a>) -> Vec> { 232 | let mut results = vec![]; 233 | let expr_str = token.val; 234 | if !(expr_str.starts_with("{") && expr_str.ends_with("}")) { 235 | results.push(match is_safe_val(token) { 236 | true => Warn(ctx, "Unquoted expr", expr_str), 237 | false => Danger(ctx, "Dangerous unquoted expr", expr_str), 238 | }); 239 | return results; 240 | }; 241 | // Technically this is the 'scan_expr' function 242 | // Expr isn't inherently dangerous, let's check functions inside the expr 243 | assert!(token.val.starts_with("{") && token.val.ends_with("}")); 244 | let expr = &token.val[1..token.val.len()-1]; 245 | let (parse, remaining) = rstcl::parse_expr(expr); 246 | assert!(parse.tokens.len() == 1 && remaining == ""); 247 | for tok in parse.tokens[0].iter().filter(|tok| tok.ttype == TokenType::Command) { 248 | results.extend(scan_command(tok.val).into_iter()); 249 | } 250 | return results; 251 | } 252 | 253 | /// Scans a TokenType::Command token (contained in '[]') for danger 254 | pub fn scan_command<'a>(string: &'a str) -> Vec> { 255 | assert!(string.starts_with("[") && string.ends_with("]")); 256 | let script = &string[1..string.len()-1]; 257 | return scan_script(script); 258 | } 259 | 260 | /// Scans a sequence of commands for danger 261 | pub fn scan_script<'a>(string: &'a str) -> Vec> { 262 | let mut all_results: Vec> = vec![]; 263 | for parse in rstcl::parse_script(string) { 264 | let results = check_command(&parse.command.unwrap(), &parse.tokens); 265 | all_results.extend(results.into_iter()); 266 | } 267 | return all_results; 268 | } 269 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(plugin)] 2 | 3 | extern crate rustc_serialize; 4 | extern crate docopt; 5 | extern crate tclscan; 6 | 7 | use std::error::Error; 8 | use std::fs; 9 | use std::io::prelude::*; 10 | use std::io; 11 | use std::path::Path; 12 | use docopt::Docopt; 13 | use tclscan::rstcl; 14 | use tclscan::CheckResult; 15 | 16 | const USAGE: &'static str = "Usage: tclscan check [--no-warn] ( - | ) 17 | tclscan parsestr ( - | )"; 18 | 19 | pub fn main() { 20 | let args = Docopt::new(USAGE) 21 | .and_then(|dopt| dopt.parse()) 22 | .unwrap_or_else(|e| e.exit()); 23 | 24 | let take_stdin = args.get_bool("-"); 25 | let cmd_check = args.get_bool("check"); 26 | let cmd_parsestr = args.get_bool("parsestr"); 27 | let flag_no_warn = args.get_bool("--no-warn"); 28 | 29 | let arg_path = args.get_str(""); 30 | let arg_script_str = args.get_str(""); 31 | 32 | let script_in = match (cmd_check, cmd_parsestr, take_stdin) { 33 | (true, false, false) => { 34 | let path = Path::new(&arg_path); 35 | let path_display = path.display(); 36 | let mut file = match fs::File::open(&path) { 37 | Err(err) => panic!("ERROR: Couldn't open {}: {}", 38 | path_display, Error::description(&err)), 39 | Ok(file) => file, 40 | }; 41 | let mut file_content = String::new(); 42 | match file.read_to_string(&mut file_content) { 43 | Err(err) => panic!("ERROR: Couldn't read {}: {}", 44 | path_display, Error::description(&err)), 45 | Ok(_) => file_content, 46 | } 47 | }, 48 | (true, false, true) | 49 | (false, true, true) => { 50 | let mut stdin_content = String::new(); 51 | match io::stdin().read_to_string(&mut stdin_content) { 52 | Err(err) => panic!("ERROR: Couldn't read stdin: {}", 53 | Error::description(&err)), 54 | Ok(_) => stdin_content, 55 | } 56 | }, 57 | (false, true, false) => arg_script_str.to_owned(), 58 | _ => panic!("Internal error: could not load script"), 59 | }; 60 | let script = &script_in; 61 | match (cmd_check, cmd_parsestr) { 62 | (true, false) => { 63 | let mut results = tclscan::scan_script(script); 64 | if flag_no_warn { 65 | results = results.into_iter().filter(|r| 66 | match r { &CheckResult::Warn(_, _, _) => false, _ => true } 67 | ).collect(); 68 | } 69 | if results.len() > 0 { 70 | for check_result in results.iter() { 71 | println!("{}", check_result); 72 | } 73 | println!(""); 74 | }; 75 | }, 76 | (false, true) => 77 | println!("{:?}", rstcl::parse_script(script)), 78 | _ => 79 | panic!("Internal error: invalid operation"), 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/mytcl.h: -------------------------------------------------------------------------------- 1 | //#define USE_TCL_STUBS 2 | #include "/usr/include/tcl/tcl.h" 3 | -------------------------------------------------------------------------------- /src/rstcl.rs: -------------------------------------------------------------------------------- 1 | use std::mem::uninitialized; 2 | use std::ffi::CString; 3 | 4 | use num::traits::FromPrimitive; 5 | 6 | use tcl; 7 | use self::TokenType::*; 8 | 9 | static mut I: Option<*mut tcl::Tcl_Interp> = None; 10 | unsafe fn tcl_interp() -> *mut tcl::Tcl_Interp { 11 | if I.is_none() { 12 | I = Some(tcl::Tcl_CreateInterp()); 13 | } 14 | return I.unwrap(); 15 | } 16 | 17 | enum_from_primitive! { 18 | #[derive(Clone, Copy, Debug, PartialEq)] 19 | pub enum TokenType { 20 | Word = 1, // TCL_TOKEN_WORD 21 | SimpleWord = 2, // TCL_TOKEN_SIMPLE_WORD 22 | Text = 4, // TCL_TOKEN_TEXT 23 | Bs = 8, // TCL_TOKEN_BS 24 | Command = 16, // TCL_TOKEN_COMMAND 25 | Variable = 32, // TCL_TOKEN_VARIABLE 26 | SubExpr = 64, // TCL_TOKEN_SUB_EXPR 27 | Operator = 128, // TCL_TOKEN_OPERATOR 28 | ExpandWord = 256, // TCL_TOKEN_EXPAND_WORD 29 | } 30 | } 31 | 32 | #[derive(Debug, PartialEq)] 33 | pub struct TclParse<'a> { 34 | pub comment: Option<&'a str>, 35 | pub command: Option<&'a str>, 36 | pub tokens: Vec>, 37 | } 38 | #[derive(Debug, PartialEq)] 39 | pub struct TclToken<'a> { 40 | pub ttype: TokenType, 41 | pub val: &'a str, 42 | pub tokens: Vec>, 43 | } 44 | impl<'b> TclToken<'b> { 45 | pub fn iter<'a>(&'a self) -> TclTokenIter<'a, 'b> { 46 | TclTokenIter { 47 | token: self, 48 | cur: 0, 49 | } 50 | } 51 | fn traverse(&self, num: usize) -> (usize, Option<&TclToken<'b>>) { 52 | if num == 0 { 53 | return (0, Some(self)); 54 | } 55 | let mut numleft = num - 1; 56 | for subtok in self.tokens.iter() { 57 | match subtok.traverse(numleft) { 58 | (0, Some(tok)) => { return (0, Some(tok)); }, 59 | (n, None) => { numleft = n; }, 60 | _ => assert!(false), 61 | } 62 | } 63 | return (numleft, None); 64 | } 65 | } 66 | pub struct TclTokenIter<'a, 'b: 'a> { 67 | token: &'a TclToken<'b>, 68 | cur: usize, 69 | } 70 | impl<'b, 'c: 'b> Iterator for TclTokenIter<'b, 'c> { 71 | type Item = &'b TclToken<'c>; 72 | fn next(&mut self) -> Option<&'b TclToken<'c>> { 73 | self.cur += 1; 74 | let ret: Option<&'b TclToken<'c>> = match self.token.traverse(self.cur-1) { 75 | (0, Some(tok)) => Some(tok), 76 | (0, None) => None, 77 | x => panic!("Invalid traverse return {:?}, iterator called after finish?", x), 78 | }; 79 | return ret; 80 | } 81 | } 82 | 83 | /// Takes: a string, which should be a tcl script 84 | /// Returns: a parse structure and the remaining string. 85 | /// 86 | /// ``` 87 | /// use tclscan::rstcl::{TclParse,TclToken}; 88 | /// use tclscan::rstcl::TokenType::{SimpleWord,Word,Variable,Text,Command}; 89 | /// use tclscan::rstcl::parse_command; 90 | /// assert!(parse_command("a b $c [d]") == (TclParse { 91 | /// comment: Some(""), command: Some("a b $c [d]"), 92 | /// tokens: vec![ 93 | /// TclToken { 94 | /// ttype: SimpleWord, val: "a", 95 | /// tokens: vec![TclToken { ttype: Text, val: "a", tokens: vec![] }] 96 | /// }, 97 | /// TclToken { 98 | /// ttype: SimpleWord, val: "b", 99 | /// tokens: vec![TclToken { ttype: Text, val: "b", tokens: vec![] }] 100 | /// }, 101 | /// TclToken { 102 | /// ttype: Word, val: "$c", 103 | /// tokens: vec![ 104 | /// TclToken { 105 | /// ttype: Variable, val: "$c", 106 | /// tokens: vec![TclToken { ttype: Text, val: "c", tokens: vec![] }] 107 | /// } 108 | /// ] 109 | /// }, 110 | /// TclToken { 111 | /// ttype: Word, val: "[d]", 112 | /// tokens: vec![TclToken { ttype: Command, val: "[d]", tokens: vec![] }] 113 | /// } 114 | /// ] 115 | /// }, "")); 116 | /// assert!(parse_command(" a\n") == (TclParse { 117 | /// comment: Some(""), command: Some("a\n"), 118 | /// tokens: vec![ 119 | /// TclToken { 120 | /// ttype: SimpleWord, val: "a", 121 | /// tokens: vec![TclToken { ttype: Text, val: "a", tokens: vec![] }] 122 | /// } 123 | /// ] 124 | /// }, "")); 125 | /// assert!(parse_command("a; b") == (TclParse { 126 | /// comment: Some(""), command: Some("a;"), 127 | /// tokens: vec![ 128 | /// TclToken { 129 | /// ttype: SimpleWord, val: "a", 130 | /// tokens: vec![TclToken { ttype: Text, val: "a", tokens: vec![] }] 131 | /// } 132 | /// ] 133 | /// }, " b")); 134 | /// assert!(parse_command("#comment\n\n\na\n") == (TclParse { 135 | /// comment: Some("#comment\n"), command: Some("a\n"), 136 | /// tokens: vec![ 137 | /// TclToken { 138 | /// ttype: SimpleWord, val: "a", 139 | /// tokens: vec![TclToken { ttype: Text, val: "a", tokens: vec![] }] 140 | /// } 141 | /// ] 142 | /// }, "")); 143 | /// ``` 144 | pub fn parse_command<'a>(string: &'a str) -> (TclParse<'a>, &'a str) { 145 | return parse(string, true, false); 146 | } 147 | /// Takes: a string, which should be a tcl script 148 | /// Returns: a list of parse structures representing the entire script 149 | /// 150 | /// ``` 151 | /// use tclscan::rstcl::TclParse; 152 | /// use tclscan::rstcl::parse_script; 153 | /// assert!(parse_script(";;; ; ;") == vec![ 154 | /// TclParse { comment: Some(""), command: Some(";"), tokens: vec![] }, 155 | /// TclParse { comment: Some(""), command: Some(";"), tokens: vec![] }, 156 | /// TclParse { comment: Some(""), command: Some(";"), tokens: vec![] }, 157 | /// TclParse { comment: Some(""), command: Some(";"), tokens: vec![] }, 158 | /// TclParse { comment: Some(""), command: Some(";"), tokens: vec![] } 159 | /// ]); 160 | /// ``` 161 | pub fn parse_script<'a>(string: &'a str) -> Vec> { 162 | let mut script = string; 163 | let mut commands = vec![]; 164 | while script.len() > 0 { 165 | let (parse, remaining) = parse_command(script); 166 | // Make sure commandless parse only happens at the end or at a semicolon 167 | assert!(parse.tokens.len() > 0 || remaining.len() == 0 || parse.command == Some(";"), 168 | "S:`{}` P:{:?} R:`{}`", script, parse, remaining); 169 | script = remaining; 170 | commands.push(parse); 171 | } 172 | return commands; 173 | } 174 | /// Takes: a string, which should be a tcl expr 175 | /// Returns: a parse structure and the remaining script. 176 | /// 177 | /// ``` 178 | /// use tclscan::rstcl::{TclParse,TclToken}; 179 | /// use tclscan::rstcl::TokenType::{SubExpr,Text,Variable,Command,Operator}; 180 | /// use tclscan::rstcl::parse_expr; 181 | /// assert!(parse_expr("[a]+$b+cos([c]+$d)") == (TclParse { 182 | /// comment: None, command: None, 183 | /// tokens: vec![ 184 | /// TclToken { 185 | /// ttype: SubExpr, val: "[a]+$b+cos([c]+$d)", 186 | /// tokens: vec![ 187 | /// TclToken { ttype: Operator, val: "+", tokens: vec![] }, 188 | /// TclToken { 189 | /// ttype: SubExpr, val: "[a]+$b", 190 | /// tokens: vec![ 191 | /// TclToken { ttype: Operator, val: "+", tokens: vec![] }, 192 | /// TclToken { 193 | /// ttype: SubExpr, val: "[a]", 194 | /// tokens: vec![ 195 | /// TclToken { ttype: Command, val: "[a]", tokens: vec![] } 196 | /// ] 197 | /// }, 198 | /// TclToken { 199 | /// ttype: SubExpr, val: "$b", 200 | /// tokens: vec![ 201 | /// TclToken { 202 | /// ttype: Variable, val: "$b", 203 | /// tokens: vec![ 204 | /// TclToken { ttype: Text, val: "b", tokens: vec![] } 205 | /// ] 206 | /// } 207 | /// ] 208 | /// } 209 | /// ] 210 | /// }, 211 | /// TclToken { 212 | /// ttype: SubExpr, val: "cos([c]+$d)", 213 | /// tokens: vec![ 214 | /// TclToken { ttype: Operator, val: "cos", tokens: vec![] }, 215 | /// TclToken { 216 | /// ttype: SubExpr, val: "[c]+$d", 217 | /// tokens: vec![ 218 | /// TclToken { ttype: Operator, val: "+", tokens: vec![] }, 219 | /// TclToken { 220 | /// ttype: SubExpr, val: "[c]", 221 | /// tokens: vec![ 222 | /// TclToken { ttype: Command, val: "[c]", tokens: vec![] } 223 | /// ] 224 | /// }, 225 | /// TclToken { 226 | /// ttype: SubExpr, val: "$d", 227 | /// tokens: vec![ 228 | /// TclToken { 229 | /// ttype: Variable, val: "$d", 230 | /// tokens: vec![ 231 | /// TclToken { ttype: Text, val: "d", tokens: vec![] } 232 | /// ] 233 | /// } 234 | /// ] 235 | /// } 236 | /// ] 237 | /// } 238 | /// ] 239 | /// } 240 | /// ] 241 | /// } 242 | /// ] 243 | /// }, "")); 244 | /// assert!(parse_expr("1") == (TclParse { 245 | /// comment: None, command: None, 246 | /// tokens: vec![ 247 | /// TclToken { 248 | /// ttype: SubExpr, val: "1", 249 | /// tokens: vec![TclToken { ttype: Text, val: "1", tokens: vec![] }] 250 | /// } 251 | /// ] 252 | /// }, "")); 253 | /// ``` 254 | pub fn parse_expr<'a>(string: &'a str) -> (TclParse<'a>, &'a str) { 255 | return parse(string, false, true); 256 | } 257 | 258 | fn parse<'a>(string: &'a str, is_command: bool, is_expr: bool) -> (TclParse<'a>, &'a str) { 259 | unsafe { 260 | let mut parse: tcl::Tcl_Parse = uninitialized(); 261 | let parse_ptr: *mut tcl::Tcl_Parse = &mut parse; 262 | 263 | // https://github.com/rust-lang/rust/issues/16035 264 | let string_cstr = CString::new(string.as_bytes()).unwrap(); 265 | let string_ptr = string_cstr.as_ptr(); 266 | let string_start = string_ptr as usize; 267 | 268 | let parsed = match (is_command, is_expr) { 269 | // interp, start, numBytes, nested, parsePtr 270 | (true, false) => tcl::Tcl_ParseCommand(tcl_interp(), string_ptr, -1, 0, parse_ptr), 271 | // interp, start, numBytes, parsePtr 272 | (false, true) => tcl::Tcl_ParseExpr(tcl_interp(), string_ptr, -1, parse_ptr), 273 | parse_args => panic!("Don't know how to parse {:?}", parse_args), 274 | }; 275 | if parsed != 0 { 276 | println!("WARN: couldn't parse {}", string); 277 | return (TclParse { comment: Some(""), command: Some(""), tokens: vec![] }, ""); 278 | } 279 | let tokens = make_tokens(string, string_start, &parse); 280 | 281 | let (tclparse, remaining) = match (is_command, is_expr) { 282 | (true, false) => { 283 | assert!(tokens.len() == parse.numWords as usize); 284 | // commentStart seems to be undefined if commentSize == 0 285 | let comment = Some(match parse.commentSize as usize { 286 | 0 => "", 287 | l => { 288 | let offset = parse.commentStart as usize - string_start; 289 | &string[offset..offset+l] 290 | }, 291 | }); 292 | let command_len = parse.commandSize as usize; 293 | let command_off = parse.commandStart as usize - string_start; 294 | let command = Some(&string[command_off..command_off+command_len]); 295 | let remaining = &string[command_off+command_len..]; 296 | (TclParse { comment: comment, command: command, tokens: tokens }, remaining) 297 | }, 298 | (false, true) => { 299 | (TclParse { comment: None, command: None, tokens: tokens }, "") 300 | }, 301 | _ => panic!("Unreachable"), 302 | }; 303 | 304 | tcl::Tcl_FreeParse(parse_ptr); 305 | return (tclparse, remaining); 306 | } 307 | } 308 | 309 | unsafe fn make_tokens<'a>(string: &'a str, string_start: usize, tcl_parse: &tcl::Tcl_Parse) -> Vec> { 310 | let mut acc = vec![]; 311 | for i in (0..tcl_parse.numTokens as isize).rev() { 312 | let tcl_token = *(tcl_parse.tokenPtr).offset(i); 313 | assert!(tcl_token.start as usize > 0); 314 | let offset = tcl_token.start as usize - string_start; 315 | let token_size = tcl_token.size as usize; 316 | let tokenval = &string[offset..offset+token_size]; 317 | make_tcltoken(&tcl_token, tokenval, &mut acc); 318 | } 319 | acc.reverse(); 320 | return acc; 321 | } 322 | 323 | fn count_tokens(token: &TclToken) -> usize { 324 | token.tokens.iter().map(|t| count_tokens(t)).sum::() + 1 325 | } 326 | 327 | fn make_tcltoken<'a>(tcl_token: &tcl::Tcl_Token, tokenval: &'a str, acc: &mut Vec>) { 328 | let token_type: TokenType = TokenType::from_usize(tcl_token.type_ as usize).unwrap(); 329 | let num_subtokens = tcl_token.numComponents as usize; 330 | 331 | let subtokens = match token_type { 332 | Word | ExpandWord => { 333 | let mut subtokens = vec![]; 334 | let mut count = 0; 335 | while count < num_subtokens { 336 | assert!(acc.len() > 0); 337 | let tok = acc.pop().unwrap(); 338 | count += count_tokens(&tok); 339 | subtokens.push(tok); 340 | } 341 | assert!(count == num_subtokens); 342 | subtokens 343 | }, 344 | SimpleWord => { 345 | assert!(acc.len() > 0); 346 | assert!(num_subtokens == 1); 347 | let tok = acc.pop().unwrap(); 348 | assert!(tok.ttype == Text); 349 | vec![tok] 350 | }, 351 | Text | Bs => { 352 | assert!(num_subtokens == 0); 353 | vec![] 354 | }, 355 | Command => { 356 | assert!(tokenval.chars().nth(0) == Some('[')); 357 | assert!(num_subtokens == 0); 358 | vec![] 359 | }, 360 | Variable => { 361 | assert!(acc.len() > 0); 362 | let tok = acc.pop().unwrap(); 363 | assert!(tok.ttype == Text); 364 | let mut subtokens = vec![tok]; 365 | let mut count = 1; 366 | while count < num_subtokens { 367 | assert!(acc.len() > 0); 368 | let tok = acc.pop().unwrap(); 369 | count += match tok.ttype { 370 | Text | Bs | Command | Variable => count_tokens(&tok), 371 | _ => panic!("Invalid token type {:?}", tok.ttype), 372 | }; 373 | subtokens.push(tok); 374 | } 375 | assert!(count == num_subtokens); 376 | subtokens 377 | }, 378 | SubExpr => { 379 | assert!(acc.len() > 0); 380 | let start_ttype = acc[acc.len()-1].ttype; 381 | let mut subtokens = vec![]; 382 | let mut count = 0; 383 | if start_ttype == Operator { 384 | subtokens.push(acc.pop().unwrap()); 385 | count += 1; 386 | } 387 | while count < num_subtokens { 388 | assert!(acc.len() > 0); 389 | let tok = acc.pop().unwrap(); 390 | if start_ttype == Operator { 391 | assert!(tok.ttype == SubExpr); 392 | } 393 | match tok.ttype { 394 | Word | Text | Bs | Command | Variable | SubExpr => { 395 | count += count_tokens(&tok) 396 | }, 397 | _ => panic!("Invalid token {:?}", tok.ttype), 398 | } 399 | subtokens.push(tok); 400 | } 401 | assert!(count == num_subtokens); 402 | subtokens 403 | }, 404 | Operator => { 405 | assert!(acc.len() > 0); 406 | assert!(num_subtokens == 0); 407 | vec![] 408 | }, 409 | }; 410 | acc.push(TclToken { val: tokenval, tokens: subtokens, ttype: token_type }) 411 | } 412 | --------------------------------------------------------------------------------