Skip to content

Instantly share code, notes, and snippets.

@tomjn
Last active April 17, 2018 19:45
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tomjn/d10a52b7a339d9d076c7 to your computer and use it in GitHub Desktop.
Save tomjn/d10a52b7a339d9d076c7 to your computer and use it in GitHub Desktop.
Adds a shortcode to WordPress that lets you test escaping and sanitising functions on content
=== Escaping Checker ===
Contributors: tjnowell
Donate link: https://tomjn.com/
Tags: security
Requires at least: 4.3
Tested up to: 4.95
Stable tag: 1.1
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
A handy tool for checking the output of escaping, sanitization and validation functions
== Description ==
This plugin adds a shortcode that when used inserts an escaping checker. The checker gives you a form you can enter potentially unsafe content into, and when submitted, passes that content through every sanitisation, validation and escaping function in WordPress Core, and some PHP Core functions
== Installation ==
1. Upload the plugin folder into the `/wp-content/plugins/` directory
1. Activate the plugin through the 'Plugins' menu in WordPress
1. Place the `[tomjn_escaping_tests]` in a page or post and save
If you prefer to create a dedicate page template, use `echo tomjn_escaping_tests();`
== Frequently Asked Questions ==
= There's a Lot of Escaping Functions! =
Yes! But not every function listed here is an escaping function, there are validation and sanitising functions too, some of them are PHP Core functions rather than WordPress functions
== Screenshots ==
1. The escaping form
== Changelog ==
= 1.1 =
* Put a green or yellow border around output depending on wether it matches the original input
* Bumped tested WP version to 4.9.5
= 1.0 =
* Added `behave.js` to make typing content easier
== Upgrade Notice ==
/*
* Behave.js
*
* Copyright 2013, Jacob Kelley - http://jakiestfu.com/
* Released under the MIT Licence
* http://opensource.org/licenses/MIT
*
* Github: http://github.com/jakiestfu/Behave.js/
* Version: 1.5
*/
(function(undefined){
'use strict';
var BehaveHooks = BehaveHooks || (function(){
var hooks = {};
return {
add: function(hookName, fn){
if(typeof hookName == "object"){
var i;
for(i=0; i<hookName.length; i++){
var theHook = hookName[i];
if(!hooks[theHook]){
hooks[theHook] = [];
}
hooks[theHook].push(fn);
}
} else {
if(!hooks[hookName]){
hooks[hookName] = [];
}
hooks[hookName].push(fn);
}
},
get: function(hookName){
if(hooks[hookName]){
return hooks[hookName];
}
}
};
})(),
Behave = Behave || function (userOpts) {
if (typeof String.prototype.repeat !== 'function') {
String.prototype.repeat = function(times) {
if(times < 1){
return '';
}
if(times % 2){
return this.repeat(times - 1) + this;
}
var half = this.repeat(times / 2);
return half + half;
};
}
if (typeof Array.prototype.filter !== 'function') {
Array.prototype.filter = function(func /*, thisp */) {
if (this === null) {
throw new TypeError();
}
var t = Object(this),
len = t.length >>> 0;
if (typeof func != "function"){
throw new TypeError();
}
var res = [],
thisp = arguments[1];
for (var i = 0; i < len; i++) {
if (i in t) {
var val = t[i];
if (func.call(thisp, val, i, t)) {
res.push(val);
}
}
}
return res;
};
}
var defaults = {
textarea: null,
replaceTab: true,
softTabs: true,
tabSize: 4,
autoOpen: true,
overwrite: true,
autoStrip: true,
autoIndent: true,
fence: false
},
tab,
newLine,
charSettings = {
keyMap: [
{ open: "\"", close: "\"", canBreak: false },
{ open: "'", close: "'", canBreak: false },
{ open: "(", close: ")", canBreak: false },
{ open: "[", close: "]", canBreak: true },
{ open: "{", close: "}", canBreak: true }
]
},
utils = {
_callHook: function(hookName, passData){
var hooks = BehaveHooks.get(hookName);
passData = typeof passData=="boolean" && passData === false ? false : true;
if(hooks){
if(passData){
var theEditor = defaults.textarea,
textVal = theEditor.value,
caretPos = utils.cursor.get(),
i;
for(i=0; i<hooks.length; i++){
hooks[i].call(undefined, {
editor: {
element: theEditor,
text: textVal,
levelsDeep: utils.levelsDeep()
},
caret: {
pos: caretPos
},
lines: {
current: utils.cursor.getLine(textVal, caretPos),
total: utils.editor.getLines(textVal)
}
});
}
} else {
for(i=0; i<hooks.length; i++){
hooks[i].call(undefined);
}
}
}
},
defineNewLine: function(){
var ta = document.createElement('textarea');
ta.value = "\n";
if(ta.value.length==2){
newLine = "\r\n";
} else {
newLine = "\n";
}
},
defineTabSize: function(tabSize){
if(typeof defaults.textarea.style.OTabSize != "undefined"){
defaults.textarea.style.OTabSize = tabSize; return;
}
if(typeof defaults.textarea.style.MozTabSize != "undefined"){
defaults.textarea.style.MozTabSize = tabSize; return;
}
if(typeof defaults.textarea.style.tabSize != "undefined"){
defaults.textarea.style.tabSize = tabSize; return;
}
},
cursor: {
getLine: function(textVal, pos){
return ((textVal.substring(0,pos)).split("\n")).length;
},
get: function() {
if (typeof document.createElement('textarea').selectionStart==="number") {
return defaults.textarea.selectionStart;
} else if (document.selection) {
var caretPos = 0,
range = defaults.textarea.createTextRange(),
rangeDupe = document.selection.createRange().duplicate(),
rangeDupeBookmark = rangeDupe.getBookmark();
range.moveToBookmark(rangeDupeBookmark);
while (range.moveStart('character' , -1) !== 0) {
caretPos++;
}
return caretPos;
}
},
set: function (start, end) {
if(!end){
end = start;
}
if (defaults.textarea.setSelectionRange) {
defaults.textarea.focus();
defaults.textarea.setSelectionRange(start, end);
} else if (defaults.textarea.createTextRange) {
var range = defaults.textarea.createTextRange();
range.collapse(true);
range.moveEnd('character', end);
range.moveStart('character', start);
range.select();
}
},
selection: function(){
var textAreaElement = defaults.textarea,
start = 0,
end = 0,
normalizedValue,
range,
textInputRange,
len,
endRange;
if (typeof textAreaElement.selectionStart == "number" && typeof textAreaElement.selectionEnd == "number") {
start = textAreaElement.selectionStart;
end = textAreaElement.selectionEnd;
} else {
range = document.selection.createRange();
if (range && range.parentElement() == textAreaElement) {
normalizedValue = utils.editor.get();
len = normalizedValue.length;
textInputRange = textAreaElement.createTextRange();
textInputRange.moveToBookmark(range.getBookmark());
endRange = textAreaElement.createTextRange();
endRange.collapse(false);
if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
start = end = len;
} else {
start = -textInputRange.moveStart("character", -len);
start += normalizedValue.slice(0, start).split(newLine).length - 1;
if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
end = len;
} else {
end = -textInputRange.moveEnd("character", -len);
end += normalizedValue.slice(0, end).split(newLine).length - 1;
}
}
}
}
return start==end ? false : {
start: start,
end: end
};
}
},
editor: {
getLines: function(textVal){
return (textVal).split("\n").length;
},
get: function(){
return defaults.textarea.value.replace(/\r/g,'');
},
set: function(data){
defaults.textarea.value = data;
}
},
fenceRange: function(){
if(typeof defaults.fence == "string"){
var data = utils.editor.get(),
pos = utils.cursor.get(),
hacked = 0,
matchedFence = data.indexOf(defaults.fence),
matchCase = 0;
while(matchedFence>=0){
matchCase++;
if( pos < (matchedFence+hacked) ){
break;
}
hacked += matchedFence+defaults.fence.length;
data = data.substring(matchedFence+defaults.fence.length);
matchedFence = data.indexOf(defaults.fence);
}
if( (hacked) < pos && ( (matchedFence+hacked) > pos ) && matchCase%2===0){
return true;
}
return false;
} else {
return true;
}
},
isEven: function(_this,i){
return i%2;
},
levelsDeep: function(){
var pos = utils.cursor.get(),
val = utils.editor.get();
var left = val.substring(0, pos),
levels = 0,
i, j;
for(i=0; i<left.length; i++){
for (j=0; j<charSettings.keyMap.length; j++) {
if(charSettings.keyMap[j].canBreak){
if(charSettings.keyMap[j].open == left.charAt(i)){
levels++;
}
if(charSettings.keyMap[j].close == left.charAt(i)){
levels--;
}
}
}
}
var toDecrement = 0,
quoteMap = ["'", "\""];
for(i=0; i<charSettings.keyMap.length; i++) {
if(charSettings.keyMap[i].canBreak){
for(j in quoteMap){
toDecrement += left.split(quoteMap[j]).filter(utils.isEven).join('').split(charSettings.keyMap[i].open).length - 1;
}
}
}
var finalLevels = levels - toDecrement;
return finalLevels >=0 ? finalLevels : 0;
},
deepExtend: function(destination, source) {
for (var property in source) {
if (source[property] && source[property].constructor &&
source[property].constructor === Object) {
destination[property] = destination[property] || {};
utils.deepExtend(destination[property], source[property]);
} else {
destination[property] = source[property];
}
}
return destination;
},
addEvent: function addEvent(element, eventName, func) {
if (element.addEventListener){
element.addEventListener(eventName,func,false);
} else if (element.attachEvent) {
element.attachEvent("on"+eventName, func);
}
},
removeEvent: function addEvent(element, eventName, func){
if (element.addEventListener){
element.removeEventListener(eventName,func,false);
} else if (element.attachEvent) {
element.detachEvent("on"+eventName, func);
}
},
preventDefaultEvent: function(e){
if(e.preventDefault){
e.preventDefault();
} else {
e.returnValue = false;
}
}
},
intercept = {
tabKey: function (e) {
if(!utils.fenceRange()){ return; }
if (e.keyCode == 9) {
utils.preventDefaultEvent(e);
var toReturn = true;
utils._callHook('tab:before');
var selection = utils.cursor.selection(),
pos = utils.cursor.get(),
val = utils.editor.get();
if(selection){
var tempStart = selection.start;
while(tempStart--){
if(val.charAt(tempStart)=="\n"){
selection.start = tempStart + 1;
break;
}
}
var toIndent = val.substring(selection.start, selection.end),
lines = toIndent.split("\n"),
i;
if(e.shiftKey){
for(i = 0; i<lines.length; i++){
if(lines[i].substring(0,tab.length) == tab){
lines[i] = lines[i].substring(tab.length);
}
}
toIndent = lines.join("\n");
utils.editor.set( val.substring(0,selection.start) + toIndent + val.substring(selection.end) );
utils.cursor.set(selection.start, selection.start+toIndent.length);
} else {
for(i in lines){
lines[i] = tab + lines[i];
}
toIndent = lines.join("\n");
utils.editor.set( val.substring(0,selection.start) + toIndent + val.substring(selection.end) );
utils.cursor.set(selection.start, selection.start+toIndent.length);
}
} else {
var left = val.substring(0, pos),
right = val.substring(pos),
edited = left + tab + right;
if(e.shiftKey){
if(val.substring(pos-tab.length, pos) == tab){
edited = val.substring(0, pos-tab.length) + right;
utils.editor.set(edited);
utils.cursor.set(pos-tab.length);
}
} else {
utils.editor.set(edited);
utils.cursor.set(pos + tab.length);
toReturn = false;
}
}
utils._callHook('tab:after');
}
return toReturn;
},
enterKey: function (e) {
if(!utils.fenceRange()){ return; }
if (e.keyCode == 13) {
utils.preventDefaultEvent(e);
utils._callHook('enter:before');
var pos = utils.cursor.get(),
val = utils.editor.get(),
left = val.substring(0, pos),
right = val.substring(pos),
leftChar = left.charAt(left.length - 1),
rightChar = right.charAt(0),
numTabs = utils.levelsDeep(),
ourIndent = "",
closingBreak = "",
finalCursorPos,
i;
if(!numTabs){
finalCursorPos = 1;
} else {
while(numTabs--){
ourIndent+=tab;
}
ourIndent = ourIndent;
finalCursorPos = ourIndent.length + 1;
for(i=0; i<charSettings.keyMap.length; i++) {
if (charSettings.keyMap[i].open == leftChar && charSettings.keyMap[i].close == rightChar){
closingBreak = newLine;
}
}
}
var edited = left + newLine + ourIndent + closingBreak + (ourIndent.substring(0, ourIndent.length-tab.length) ) + right;
utils.editor.set(edited);
utils.cursor.set(pos + finalCursorPos);
utils._callHook('enter:after');
}
},
deleteKey: function (e) {
if(!utils.fenceRange()){ return; }
if(e.keyCode == 8){
utils.preventDefaultEvent(e);
utils._callHook('delete:before');
var pos = utils.cursor.get(),
val = utils.editor.get(),
left = val.substring(0, pos),
right = val.substring(pos),
leftChar = left.charAt(left.length - 1),
rightChar = right.charAt(0),
i;
if( utils.cursor.selection() === false ){
for(i=0; i<charSettings.keyMap.length; i++) {
if (charSettings.keyMap[i].open == leftChar && charSettings.keyMap[i].close == rightChar) {
var edited = val.substring(0,pos-1) + val.substring(pos+1);
utils.editor.set(edited);
utils.cursor.set(pos - 1);
return;
}
}
var edited = val.substring(0,pos-1) + val.substring(pos);
utils.editor.set(edited);
utils.cursor.set(pos - 1);
} else {
var sel = utils.cursor.selection(),
edited = val.substring(0,sel.start) + val.substring(sel.end);
utils.editor.set(edited);
utils.cursor.set(pos);
}
utils._callHook('delete:after');
}
}
},
charFuncs = {
openedChar: function (_char, e) {
utils.preventDefaultEvent(e);
utils._callHook('openChar:before');
var pos = utils.cursor.get(),
val = utils.editor.get(),
left = val.substring(0, pos),
right = val.substring(pos),
edited = left + _char.open + _char.close + right;
defaults.textarea.value = edited;
utils.cursor.set(pos + 1);
utils._callHook('openChar:after');
},
closedChar: function (_char, e) {
var pos = utils.cursor.get(),
val = utils.editor.get(),
toOverwrite = val.substring(pos, pos + 1);
if (toOverwrite == _char.close) {
utils.preventDefaultEvent(e);
utils._callHook('closeChar:before');
utils.cursor.set(utils.cursor.get() + 1);
utils._callHook('closeChar:after');
return true;
}
return false;
}
},
action = {
filter: function (e) {
if(!utils.fenceRange()){ return; }
var theCode = e.which || e.keyCode;
if(theCode == 39 || theCode == 40 && e.which===0){ return; }
var _char = String.fromCharCode(theCode),
i;
for(i=0; i<charSettings.keyMap.length; i++) {
if (charSettings.keyMap[i].close == _char) {
var didClose = defaults.overwrite && charFuncs.closedChar(charSettings.keyMap[i], e);
if (!didClose && charSettings.keyMap[i].open == _char && defaults.autoOpen) {
charFuncs.openedChar(charSettings.keyMap[i], e);
}
} else if (charSettings.keyMap[i].open == _char && defaults.autoOpen) {
charFuncs.openedChar(charSettings.keyMap[i], e);
}
}
},
listen: function () {
if(defaults.replaceTab){ utils.addEvent(defaults.textarea, 'keydown', intercept.tabKey); }
if(defaults.autoIndent){ utils.addEvent(defaults.textarea, 'keydown', intercept.enterKey); }
if(defaults.autoStrip){ utils.addEvent(defaults.textarea, 'keydown', intercept.deleteKey); }
utils.addEvent(defaults.textarea, 'keypress', action.filter);
utils.addEvent(defaults.textarea, 'keydown', function(){ utils._callHook('keydown'); });
utils.addEvent(defaults.textarea, 'keyup', function(){ utils._callHook('keyup'); });
}
},
init = function (opts) {
if(opts.textarea){
utils._callHook('init:before', false);
utils.deepExtend(defaults, opts);
utils.defineNewLine();
if (defaults.softTabs) {
tab = " ".repeat(defaults.tabSize);
} else {
tab = "\t";
utils.defineTabSize(defaults.tabSize);
}
action.listen();
utils._callHook('init:after', false);
}
};
this.destroy = function(){
utils.removeEvent(defaults.textarea, 'keydown', intercept.tabKey);
utils.removeEvent(defaults.textarea, 'keydown', intercept.enterKey);
utils.removeEvent(defaults.textarea, 'keydown', intercept.deleteKey);
utils.removeEvent(defaults.textarea, 'keypress', action.filter);
};
init(userOpts);
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = Behave;
}
if (typeof ender === 'undefined') {
this.Behave = Behave;
this.BehaveHooks = BehaveHooks;
}
if (typeof define === "function" && define.amd) {
define("behave", [], function () {
return Behave;
});
}
}).call(this);
<?php
/*
Plugin Name: Toms Escaping tester
Plugin URI: https://tomjn.com
Description: Provides a tomjn_esc_test shortcode that lets you test values with escaping
Author: Tom J Nowell
Version: 1.1
Author URI: https://www.tomjn.com/escaping
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
function tomjn_escaping_tests(){
$value = (!empty( $_POST['tomjn_esc_value'] ) ) ? $_POST['tomjn_esc_value'] : false;
$esc_array = array();
if ( $value ) {
global $wpdb;
$esc_array = array(
'esc_html' => esc_html( $value ),
'esc_attr' => esc_attr($value),
'esc_js' => esc_js($value),
'esc_textarea' => esc_textarea( $value ),
'esc_url' => esc_url($value),
'esc_url_raw' => esc_url_raw($value),
'esc_sql' => esc_sql( $value ),
'wp_json_encode' => wp_json_encode($value),
'wp_kses( , array(), array())' => wp_kses( $value, array(), array()),
'wp_kses_data' => wp_kses_data( $value ),
'wp_kses_post' => wp_kses_post( $value ),
'wp_strip_all_tags' => wp_strip_all_tags($value),
'strip_tags' => strip_tags($value),
'htmlentities' => htmlentities($value),
'urlencode' => urlencode($value),
'rawurlencode' => rawurlencode($value),
'sanitize_email' => sanitize_email($value),
'sanitize_file_name' => sanitize_file_name($value),
'sanitize_html_class' => sanitize_html_class($value),
'sanitize_key' => sanitize_key($value),
/*'sanitize_meta' => sanitize_meta($value), doesn't make sense to run */
'sanitize_mime_type' => sanitize_mime_type($value),
/*'sanitize_option' => sanitize_option($value), this needs 2 values, and sanitising depends on param 1 */
/*'sanitize_post' => sanitize_post($value), this isn't a WP_Post object */
'sanitize_sql_orderby' => sanitize_sql_orderby($value),
/*'sanitize_term' => sanitize_term($value), expects a WP_Term and a taxonomy*/
/*'sanitize_term_field' => sanitize_term_field($value), same as above*/
'sanitize_text_field' => sanitize_text_field($value),
'sanitize_title' => sanitize_title($value),
'sanitize_title_for_query' => sanitize_title_for_query($value),
'sanitize_title_with_dashes' => sanitize_title_with_dashes( $value ),
'sanitize_user' => sanitize_user( $value ),
'balanceTags' => balanceTags( $value ),
'tag_escape' => tag_escape( $value ),
'addslashes' => addslashes( $value ),
'$wpdb->esc_like' => $wpdb->esc_like( $value ),
'$wpdb->prepare' => $wpdb->prepare( $value, array() ),
);
}
wp_enqueue_script( 'behavejs', plugins_url( 'behave.js', __FILE__ ), array(), '1.5', true );
ob_start();
?>
<form action="" method="post">
<p>
<textarea id="tomjn_escape_textarea" name="tomjn_esc_value"><?php
if ( $value ) {
echo wp_unslash( esc_html( $esc_array['esc_textarea'] ) );
}
?></textarea>
</p>
<style>
#tomjn_escape_textarea {
font-family: monospace;
min-height: 200px;
}
.tomjn_escape_same {
outline: 2px solid rgb(163, 190, 140);
}
.tomjn_escape_different {
outline: 2px solid rgb(235, 203, 139);
}
</style>
<script>
jQuery(document).ready( function() {
BehaveHooks.add(['keydown'], function(data){
var numLines = data.lines.total,
fontSize = parseInt( getComputedStyle(data.editor.element)['font-size'] ),
padding = parseInt( getComputedStyle(data.editor.element)['padding'] );
data.editor.element.style.height = (((numLines*fontSize)+padding))+'px';
});
var editor = new Behave({
textarea: document.getElementById('tomjn_escape_textarea'),
replaceTab: true,
softTabs: false,
tabSize: 4,
autoOpen: true,
overwrite: true,
autoStrip: true,
autoIndent: true
});
});
</script>
<p><input type="submit" value="Escape"></p>
</form>
<p>Refresh rather than re-submit if you're having problems with expanding quote escaping</p>
<?php
if ( $value ) {
?>
<dl>
<?php
foreach($esc_array as $key => $val) {
$class = 'tomjn_escape_different';
if ( $val === $value ) {
$class = 'tomjn_escape_same';
}
?>
<dt><?php echo esc_html( $key ); ?></dt>
<dd><pre class="<?php echo esc_attr( $class ); ?>"><?php echo htmlspecialchars( $val ); ?></pre></dd>
<?php
}
?>
</dl>
<?php
}
return ob_get_clean();
}
add_shortcode( 'tomjn_esc_test', 'tomjn_escaping_tests' );
@richardtape
Copy link

You mad, mad genius. Love this, thanks for building it.

I wonder if we can figure out a way to show the differences in the output from each of them, too, so people can know what to expect.

@zackkatz
Copy link

zackkatz commented Oct 2, 2015

Nice! I suggest running the output through htmlspecialchars() so the escaped value differences are visible to the eye.

@tfrommen
Copy link

tfrommen commented Oct 2, 2015

+1 for @zackkatz's suggestion. I just wanted to write the same. Also, the wrapping esc_html() tempers too much with the actual results.
And the last one is $wpdb->prepare (and not addslashes).

But still, very handy. :)

@JeppeSigaard
Copy link

Here's a leaner version, friend: https://gist.github.com/JeppeSigaard/1a439d2af6d4615c41c8
Just reorganizing, not really concerned with the validity of the output. Hope you like.

@tomjn
Copy link
Author

tomjn commented Dec 26, 2015

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment