-
-
Save joelpittet/7bf0190a678a691fe188 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env php | |
<?php | |
// ag --nonumbers --nofilename -C0 '[^\w](format_string|t)\(.+=\\?"[!@%][^"\W]+".*, array\([^\$]+\$' > ../modules.txt | |
// composer require pear/console_table | |
include 'vendor/autoload.php'; | |
// Read in all the lines. | |
fopen("php://stdin", "r"); | |
$text = ''; | |
while (false !== ($line = fgets(STDIN))) { | |
$text .= $line . "\n"; | |
} | |
preg_match_all('#[ts]\([\'"](.*)[\'"],\s*array\((.*)\)\s*\)#em', $text, $matches); | |
$suspects = []; | |
if (empty($matches[1]) || empty($matches[2])) { | |
print 'Something went wrong we have nothing.'; | |
} | |
elseif (count($matches[1]) != count($matches[2])) { | |
print 'Not enough arguments to match strings'; | |
} | |
else { | |
foreach ($matches[1] as $key => $text) { | |
$args = get_arguments($matches[2][$key]); | |
$non_protocol_risky_attributes = [ | |
'id', | |
'class', | |
'language', | |
]; | |
// Check if any of the arguments are in attributes. | |
foreach (array_keys($args) as $placeholder) { | |
$matched = preg_match('#(\w+)="' . preg_quote($placeholder) . '"#', $text, $attribute_match); | |
if ($matched && !empty($attribute_match[1])) { | |
$arg = $args[$placeholder]; | |
$attribute_name = $attribute_match[1]; | |
// Setup a base assumptions: | |
if ($placeholder[0] == '@') { | |
$result = 'potentially unsafe'; | |
} | |
else { | |
$result = 'likely unsafe'; | |
} | |
// Escaped title attributes are safe. | |
if ($placeholder[0] == '%') { | |
$result = 'WTF %'; | |
} | |
elseif ($placeholder[0] == '@' && $attribute_name == 'title') { | |
$result = 'safe'; | |
} | |
elseif (in_array($attribute_name, $non_protocol_risky_attributes)) { | |
// Really it should be @ check but likely not user variables. | |
// Not a protocol security risk. | |
if ($placeholder[0] == '!') { | |
$result = 'safeish'; | |
} | |
else { | |
$result = 'safe'; | |
} | |
} | |
elseif (strpos($arg, '$') === FALSE) { | |
// No variable for this arg. It's safe. | |
$result = 'safe'; | |
} | |
else { | |
// Check which variable is being used. | |
preg_match_all('#\$[\w]+(\[[\w\"\']+\]|->[\w\{\}\$]+)?#', $arg, $arg_matches); | |
$is_safe = TRUE; // Innocent till proven guilty. | |
$safe_variables = [ | |
// Theme globals. | |
"\$GLOBALS['theme']", | |
'$base_url', | |
'$base_path', | |
// Help URLS | |
'$help_url', | |
"\$data['help']", | |
"\$store_help_url", | |
'$config_path', | |
]; | |
$safe_variable_patterns = '#^(\$node\->(type|nid)|\$\w+\->[a-z]id|\$\w+\->machine_name)$#'; | |
foreach ($arg_matches[0] as $arg_match) { | |
if (in_array($arg_match, $safe_variables)) { | |
$is_safe = TRUE; | |
continue; | |
} | |
if (preg_match($safe_variable_patterns, $arg_match)) { | |
$is_safe = TRUE; | |
continue; | |
} | |
// Not trustworthy. | |
$is_safe = FALSE; | |
break; | |
} | |
// Double check any non-safe. | |
if (!$is_safe) { | |
if (strpos($arg, 'url(') === 0) { | |
$result = 'eh?'; | |
} | |
if (( | |
strpos($arg, 'check_url(') === 0 | |
|| strpos($arg, 'url(') === 0 | |
) | |
&& $arg[strlen($arg) - 1] === ')') { | |
$result = 'safe'; | |
} | |
elseif (strpos($arg, 'file_create_url(') === 0 | |
|| strpos($arg, 'check_url(') !== FALSE) { | |
$result = 'likely safe'; | |
} | |
} | |
else { | |
$result = 'safe'; | |
} | |
} | |
$suspects[] = [ | |
'placeholder' => $placeholder, | |
'text' => $text, | |
'attr' => $attribute_name, | |
'argument' => substr($arg, 0, 60), | |
'result' => $result, | |
]; | |
} | |
} | |
} | |
} | |
// var_dump($suspects); | |
usort($suspects, function ($a, $b) { | |
return strcmp($a["result"], $b["result"]) + strcmp($a["placeholder"], $b["placeholder"]); | |
}); | |
// Sort the results. | |
$sort = []; | |
foreach($suspects as $k => $v) { | |
$sort['result'][$k] = $v['result']; | |
$sort['placeholder'][$k] = $v['placeholder']; | |
} | |
# sort by result desc and then placeholder asc | |
array_multisort($sort['result'], SORT_ASC, $sort['placeholder'], SORT_ASC, $suspects); | |
$tbl = new Console_Table(); | |
$tbl->setHeaders(['Attr', 'Placeholder', 'Argument', 'Context', 'Result']); | |
$safe_stats = []; | |
foreach ($suspects as $suspect) { | |
if (!isset($safe_stats[$suspect['result']])) { | |
$safe_stats[$suspect['result']] = 0; | |
} | |
$safe_stats[$suspect['result']]++; | |
$search_string = $suspect['attr'] . '="' . $suspect['placeholder']; | |
$index = strpos($suspect['text'], $search_string); | |
$context_before = 3; | |
$context_after = 5; | |
$start_index = ($index - $context_before) > 0 ? ($index - $context_before) : 0; | |
$context = substr($suspect['text'], $start_index, strlen($search_string) + $context_after); | |
$tbl->addRow([ | |
$suspect['attr'], | |
$suspect['placeholder'], | |
$suspect['argument'], | |
$context, | |
$suspect['result'], | |
]); | |
} | |
print $tbl->getTable(); | |
print "Suspects: " . count($suspects) . "\n"; | |
print "----------------------------------\n"; | |
foreach ($safe_stats as $name => $count) { | |
print ucwords($name) . ": " . $count . "\n"; | |
} | |
function get_arguments($input) { | |
$return = []; | |
$items = str_getcsv($input); | |
foreach ($items as $item) { | |
if (strpos($item, '=>') === FALSE) { | |
print "missing assoc: $item\n"; | |
var_dump($input); | |
continue; | |
} | |
list($key, $value) = explode('=>', $item); | |
$key = trim($key); | |
$value = trim($value); | |
$key = trim($key, '"\''); | |
$return[$key] = $value; | |
} | |
return $return; | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment