class Analyzer::Cfml::Pure

Overview

Plain CFML (ColdFusion / Lucee / BoxLang) attack surface.

CFML has two directly-reachable shapes and no route table:

Framework route tables (Taffy taffy:uri, ColdBox Router.cfc, Wheels config/routes.cfm, FW/1 variables.framework.routes) are deliberately out of scope here — they warrant their own techs so the generic analyzer isn't the thing that decides framework semantics.

Defined in:

analyzer/analyzers/cfml/pure.cr

Constant Summary

CFPARAM_ASSIGN_RE = /(?<![.\w])param\s+(url|form|cookie)\.(\w+)\s*=/i
CFPARAM_SCRIPT_RE = /(?<![.\w])param\b[^;\n]*?\bname\s*=\s*["'](url|form|cookie)\.(\w+)["']/i
CFPARAM_TAG_RE = /<cfparam\b[^>]*?\bname\s*=\s*["'](url|form|cookie)\.(\w+)["']/i
CGI_HEADER_RE = /(?<![.\w])cgi\.(http_[a-z_]+)/i

cgi.http_x_forwarded_for is the CFML spelling of an inbound header. The other cgi.* keys (request_method, script_name, ...) are server metadata, not request params.

INTERPOLATION_RE = /#[^#\n]+#/
IS_DEFINED_RE = /\bisDefined\s*\(\s*["'](url|form|cookie)\.(\w+)["']\s*\)/i
LIFECYCLE_PAGES = Set {"application.cfm", "onrequestend.cfm"}

Auto-run lifecycle includes, not requestable pages — the CFML analogue of global.asa.

REMOTE_HINT_RE = /remote/i

CFML is case-insensitive, so access="ReMote" is as valid as access="remote"; gate on one case-insensitive pass rather than a handful of literal spellings.

SCOPE_BRACKET_RE = /#{SCOPE_PREFIX}\s*\[\s*["']([^"'\]]+)["']\s*\]/i
SCOPE_DOT_RE = /#{SCOPE_PREFIX}\.([A-Za-z_]\w*)/i
SCOPE_PATTERNS = [CFPARAM_TAG_RE, CFPARAM_SCRIPT_RE, CFPARAM_ASSIGN_RE, SCOPE_DOT_RE, SCOPE_BRACKET_RE, STRUCT_KEY_EXISTS_RE, IS_DEFINED_RE]
SCOPE_PREFIX = "(?<![.\\w])(url|form|cookie)"

Request-scope reads on .cfm pages. The negative lookbehind keeps document.form.x / application.form.x from registering as a form scope read.

SCRIPT_ACCESS_REMOTE_RE = /\baccess\s*=\s*["']remote["']/i
SCRIPT_BLOCK_RE = /(<script\b[^>]*>)([\s\S]*?)(<\/script>)/i

Client-side JavaScript routinely names a local form (form.submit(), form.action), which the scope patterns above would otherwise read as CFML form-scope access — inventing params and flipping the page to POST. Script bodies are blanked, except #...# spans, because CFML genuinely interpolates server values into JS (var id = #url.id#;).

SCRIPT_REMOTE_PREFIX_RE = /(?<![\w.])remote\s+(?:\w+\s+)?function\s+(\w+)\s*\(/i

Script syntax has two spellings of the same thing. The prefix form is matched loosely and its argument list is delimited by real paren matching; the suffix form is driven off the access attribute so a component with hundreds of private methods costs one scan, not one paren walk per declaration.

SCRIPT_REMOTE_SUFFIX_RE = /(?<![\w.])function\s+(\w+)\s*\(([^)]*)\)([^{;]*?)access\s*=\s*["']remote["']/i
STRUCT_KEY_EXISTS_RE = /\bstructKeyExists\s*\(\s*(url|form|cookie)\s*,\s*["']([^"']+)["']\s*\)/i
WEBROOT_MARKERS = ["webroot/", "wwwroot/", "htdocs/"]

Only unambiguous web-root directory names. Bare public/ and www/ were tried and dropped: they collide with build output such as docs/public/, the same false positive FileHelper documents.

Locating the root from Application.cfc was also tried and is worse — every CFML sub-application, module and test harness ships one (7-25 per repo across the validation corpus), so anchoring collapsed Taffy's 20 example apps onto a single /index.cfm. Leaving the path repo-relative keeps colliding pages distinct and locatable.

Instance Method Summary

Instance methods inherited from class Analyzer

analyze analyze, base_path : String base_path, base_paths : Array(String) base_paths, callees_needed? : Bool callees_needed?, http_header_name(name : String) : String | Nil http_header_name, line_number_for_index(content : String, char_index : Int32) : Int32 line_number_for_index, logger : NoirLogger logger, parallel_analyze(files : Array(String), &block : String -> Nil) parallel_analyze, read_file_content(path : String) : String read_file_content, result : Array(Endpoint) result, unique_params(params : Array(Param)) : Array(Param) unique_params, url : String url, web_root_path(path : String, markers : Array(String)) : String web_root_path

Constructor methods inherited from class Analyzer

new(options : Hash(String, YAML::Any)) new

Instance methods inherited from module FileHelper

all_files : Array(String) all_files, get_files_by_extension(extension : String) : Array(String) get_files_by_extension, get_files_by_extensions(extensions : Array(String)) : Array(String) get_files_by_extensions, get_files_by_prefix(prefix : String) : Array(String) get_files_by_prefix, get_files_by_prefix_and_extension(prefix : String, extension : String) : Array(String) get_files_by_prefix_and_extension, get_public_dir_files(base_path : String, folder : String) : Array(String) get_public_dir_files, get_public_files(base_path : String, anchors : Array(String) = ["shard.yml", "Gemfile"]) : Array(String) get_public_files

Instance Method Detail

def analyze #

[View source]