Determining what versions of WordPress you are hosting

If you host lots of different sites for people, one of the things you might want to know is what versions of WordPress each site is running.

WordPress stores the version number in a variable named $wp_version which is set in the file version.php.

With that information in hand, you can write a bash command that you run from your /home directory to display all of the WordPress versions you have on your server:

find . -name version.php -type f|xargs grep ^\$wp_version

This is one of the aliases I have in my .bashrc file.

Building Up Reporting Tools

While working with Lee Newton over at b5media I was able to watch him build up some server tools over time that were invaluable to diagnosing exactly what was going on on the server.

Now I find I need to make some of my own. Here’s how I am doing it.

For now I am going to concentrate on access logs.

Where these logs are varies server by server, but if you are running a standard cPanel setup, chances are you can find a directory named /usr/local/apache/domlogs with files in it named after your domain name. In this case I picked one of the sites I host: nakedpastor.com

So if I do a:

cd /usr/local/apache/domlogs
tail -10 nakedpastor.com

I will get the last 10 lines of the access log file

Here’s one example:

24.555.555.27 – – [09/Aug/2010:20:13:45 -0400] “GET /wp-content/uploads/2010/08/IMG_0001.jpg HTTP/1.1” 304 – “http://www.nakedpastor.com/” “Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_1_3 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7E18 Safari/528.16”

Someone on an iPhone is looking at a picture on the site (which David would get more Google Juice from if he had named it better).

The trick is going to be to break down that line into important bits of information that will help me diagnose how my server is being used. For example, I might want to know if one IP address is flooding me. I might want to know if I am getting a HUGE number of requests for one particular file or if I am serving a large number of errors. If I ran this on a combined log file I, I would want to know if one domain was getting all of the traffic. Top referrers might be a fun thing to look at too. There are lots of little bits of info in there that could be helpful.

My tool chest includes:

tail – request a certain number of lines from the END of the file as shown above. During testing I will use -15 to get the last 15 lines but when I make this live, I’ll want to look at the last several thousand at least.
head – requests the number of lines at the top of a file. In this case it gives me the most pertinent results
sort – Will put the most important results at the top
cut – probably not helpful initially as the fields are not fixed width
awk – Used to parse the lines into chunks so I can see what is important. See also here
grep – Used to search for text
uniq – Used with -c uniq counts the number of occurrences of each variance
| – The piping symbol used to send the results of one command right into the next.

Let’s go after something simple first. The IP address. I want to take the last 100 lines of the error log, get the the ip address which will be the first word in the line, count how many times each ip address is used, sort it numerically by count and return the top 10. In bash, that is pronounced as:

tail -10000 /usr/local/apache/domlogs/nakedpastor.com | awk ‘{print $1}’ | sort | uniq -c | sort -nr | head -10

You can find examples of that line lots of places out there. In fact I copied and pasted that from another site. Their line used a “tail -n” instead of “head”, but it did the same thing You may want to note that you have to call sort before you call uniq in order for unique to work right..

For the rest of the examples I’m going to use the awk command to break down the line into separate fields separated by quotes or spaces. this line prints the number 7 because there are 7 fields separated by quotes:

tail -5 nakedpastor.com|awk -F ‘”‘ ‘{c=NF; print c}’

If I search for quotes and then search for space, I can get the result code

tail -15 nakedpastor.com|awk -F ‘”‘ ‘{print $3}’|awk ‘{print $1}’

Or the requested page:

tail -15 nakedpastor.com|awk -F ‘”‘ ‘{print $2}’|awk ‘{print $2}’

Or the referrer:

tail -15 nakedpastor.com|awk -F ‘”‘ ‘{print $4}’|awk ‘{print $1}’

Or the agent:

tail -15 nakedpastor.com|awk -F ‘”‘ ‘{print $6}’|awk ‘{print $1}’

So using these examples I can get the 20 most popular agent/OS combos:

tail -1000 /usr/local/apache/domlogs/nakedpastor.com | awk -F ‘”‘ ‘{print $6}’| sort | uniq -c | sort -nr | head -20

So, there you have some tools to use the next time you want to see what is going on on your server. Along with free -m and top you can get some neat info.

Just remember that you are getting snapshots. When I looked a little bit ago. Knowmore.com’s bot took up 1390 of the 10000 lines I was looking at. That’s over 10% of the traffic going to just that bot. HOWEVER if I looked at 5000 lines, they didn’t appear at all. Looking at 100000 lines they only appeared 1394 times. So, don’t just use one sample size. It can be misleading.

I will probably take these lines and combine them into some .bachrc functions along the line of what I’d discussed in my Three helpful additions to your .bashrc post.

WordPress Security – a plugin done right.

It’s rare that I open the source code for a random plugin and see every recommended security measure taken. When looking at Chris Boyd‘s plugin GeoLocation Plugin, I kept digging deeper and deeper and found he’d consistently covered everything. This plugin is a text book example of how to write a secure plugin.

Since he has so many techniques in here, this post wrote itself in my head. It was irresistible.

I strongly recommend you download the plugin and open geolocation.php to follow along

So, here are the security methods I saw him use, there may be more I didn’t catch, if so feel free to add them to the comments:

Use nonces on all input forms

Chances are your plugin will ask for information. A nonce is a security token that helps guarantee that you are getting real data from a real user. In function geolocation_inner_custom_box() Chris calls

echo '<input type="hidden" id="geolocation_nonce" name="geolocation_nonce" 
	value="' .     wp_create_nonce(plugin_basename(__FILE__) ) . '" />';

to add the nonce to his input form and then to check this on the processing side in function geolocation_save_postdata he calls:

  // Check authorization, permissions, autosave, etc
  if (!wp_verify_nonce($_POST['geolocation_nonce'], plugin_basename(__FILE__)))
    return $post_id;

This is very simple code. There’s almost no cost to adding nonces to form processing and you can use that exact code, just changing the name of the field. This will block most bots from abusing your plugin.

Verify the user’s permissions

The user_can functions go hand in hand with the nonces and they work together to block the same kind of attacks. If your plugin does something that just anyone off the street shouldn’t be able to do, verify that the user is allowed to do it. This check is just a little bit further into function geolocation_save_postdata:

  if('page' == $_POST['post_type'] ) {
    if(!current_user_can('edit_page', $post_id))
		return $post_id;
  } else {
    if(!current_user_can('edit_post', $post_id))
		return $post_id;
  }

This simple check to confirm that the user is allowed to edit a post before the geotagging information is added and ties the plugin directly into all of the security already built into WordPress. It is tremendously powerful.

Configure default values

In programming 101, you learn never to trust the computer to give you a clean value for an uninitialized variable. In web development, there are even more implications to this. Caching is one of those. If you call get_option and WordPress does not have a value, it will access the database to see if there is a value set. Until a value is stored in the database, WordPress has to keep looking for it on every page load. Anything you can do that avoids accessing the database files and quite possibly the hard drives where the information is stored, will make your site run faster. If a value is found, that value will be loaded from the cache automatically. Additionally, if you set your default values, you know what you are getting back. Chris handles this in function default_settings():

function default_settings() {
	if(get_option('geolocation_map_width') == '0')
		update_option('geolocation_map_width', '450');

	if(get_option('geolocation_map_height') == '0')
		update_option('geolocation_map_height', '200');

	if(get_option('geolocation_default_zoom') == '0')
		update_option('geolocation_default_zoom', '16');

	if(get_option('geolocation_map_position') == '0')
		update_option('geolocation_map_position', 'after');
}

Use the Settings API and let WordPress do all the work

WordPress 2.7 added a wonderful set of tools called the Settings API. Most plugins do not take advantage of this complex tool set when they could. Chris uses the register_setting API call in function register_settings to enumerate the type of data he wants in each field.

function register_settings() {
  register_setting( 'geolocation-settings-group', 
		'geolocation_map_width', 'intval' );
  register_setting( 'geolocation-settings-group', 
		'geolocation_map_height', 'intval' );
  register_setting( 'geolocation-settings-group', 
		'geolocation_default_zoom', 'intval' );
  register_setting( 'geolocation-settings-group', 
		'geolocation_map_position' );
  register_setting( 'geolocation-settings-group', 
		'geolocation_wp_pin');
}

As you can see he specifies that certain values must be integers. Chris doesn’t take full advantage of the API in this plugin. In fact, I’m not convinced that simply calling register_setting on its own does anything. It is meant to be used with other calls. Had he used them all WordPress could have handled all sorts of things for him automatically, including the nonce field and additional checks.

The settings API is POWERFUL but has a steep learning curve. Once you pass the learning curve your input form printing just becomes five lines:

<form action="options.php" method="post">
	<?php settings_fields('plugin_options'); ?>
	<?php do_settings_sections(__FILE__); ?>
	<input name="Submit" type="submit" 
		value="<?php esc_attr_e('Save Changes'); ?>" />
</form>

This topic is WAAAAAAY to big to cover here and I freely admit, I’m no expert on it. I recommend that you read more: A quick & dirty example or The Whole Shebang

Verify your data

The last two topics are related as the come from the same rule: ALL data that you get from ANY source must be verified. This is where many plugins fail. The assumption is that “if it is in the database, it must be clean” or “it’s from an RSS feed, I know the data is good” or “The variable has INT in the name so it obviously contains an integer” or any of a thousand other gotyas. The way to keep yourself safe from an assumption is to VERIFY.
I want to highlight just two examples of this in Chris’s code.

First, in function display_location, he uses a cast to ensure the data he gets is what he wants from post meta:

$public = (bool)get_post_meta($post->ID, 'geo_public', true);

Second a function clean_coordinates uses regex to ensure the value he receives is what he wants:

$latitude = clean_coordinate($_POST['geolocation-latitude']);
[..]
function clean_coordinate($coordinate) {
	$pattern = '/^(\-)?(\d{1,3})\.(\d{1,15})/';
	preg_match($pattern, $coordinate, $matches);
	return $matches[0];
}

Escape all variable data you display

I won’t show specific examples of this as they are everywhere throughout the whole plugin, but this is one of the most important steps you can take to make your plugin secure. It is also one of the areas that has has the most focus in the WordPress core over the years.

If you are displaying a value that isn’t hard coded in your source, you need to first feed it through one of these functions (or something similar):

esc_attr()	Cleans HTML attributes for use on screen
esc_html()	Prepares complete blocks of HTML code
esc_js()	Special javascript handling of quotes and EOL characters
esc_sql()	Escapes data for use in a query
esc_url()	Returns a valid URL if possible
esc_url_raw()	Prepares an URL to be inserted in a database

This applies to values you are putting into javascript, urls you are displaying on the screen or using for includes, and data used to build image references. All this information must be sanitized. Visit the codex for more information on data validation.

Bonus Tip

If you’ve made it this far in the post, you get one more tip for free. DON’T DO IT ALL YOURSELF. I’d be willing to wager that this plugin was not ‘born’ with all of these security techniques in place. Though this plugin is attributed to Chris in the repository, he lists multiple authors for the plugin. You don’t HAVE to do all of this by yourself. Once your plugin is done, ask people to review it. I can pretty much guarantee that someone will find something. That’s just how it works. But won’t you feel much better if your friend catches the problem before it is discovered because your plugin added an avenue of attack to every site it’s been installed on? Someone will be glad to review it for you it. That’s part of what is great about the WordPress community: people love to help.

Summation

Website security will forever be an on going battle. It is a big, complicated subject with nuances you can miss easily. However, WordPress has been around long enough to have build up quite a collection of tools you have at your disposal as a plugin author. If use the tools WordPress provides, following Chris’s example outlined here, you can rest easy at night knowing you’ve done your job well.

Three helpful additions to your .bashrc

I just made a change to my .bashrc file and I thought I would share the tip. All of this is pretty basic stuff, but if you don’t customize your Linux logins, this would be a good place to start.

For Microsoft people who don’t know, .bashrc is in some ways like a combined config.sys and autoexec.bat file. If you don’t know what an autoexec.bat file is, you totally missed the 80s dudes…

In a *nix environment, the rc at the end of a file name typically means that it is a “run control” file. Run Control files execute when a program starts. In this case, the program is bash – the command line interpreter/shell. Other programs look for rc files too. Because of this, you could have bunches of them in your home directory. The . at the front of the file name indicates that they are hidden from a normal directory listing. This way they don’t clutter up your home.

I have lots of neat things in my .bashrc file that add functionality to my default CLI. I’ll be sharing just three of those with you now.

The first is an alias: ebrc. When I type ebrc and press enter, I’m taken immediately into an editor with my .bashrc file open. You can think of an alias as a single line shortcut. It looks like this:

alias ebrc='vi ~/.bashrc'

As you can see, it just says “when I type ‘ebrc’, treat it like I really typed ‘vi ~/.bashrc'”.

The second thing I used tonight was the alias brc:

alias brc='. ~/.bashrc'

That executes the .bashrc file again, so that all of the changes I’d just made are loaded.

You might ask “Can’t you just type all that out? You’re not saving much time.” Go ahead.. ask. I’ll wait…

OK. The answer is Yes. So, it is important that you don’t go overboard on this stuff. If you use aliases too much, you’ll lose your familiarity with *nix and the skills to do your work on any other server. So proceed with caution. This stuff can be addictive and detrimental to your guru health.

Now with those two helper aliases in hand, I added the function I really wanted to include: ‘upskel’. It takes a task I might otherwise put off and allows it to be completed in 7 keypresses. This is the perfect use case for a .bashrc function.

‘upskel’ takes the latest version of WordPress and places it into the cpanel skeleton directory that is used as the base for every new account created on my hosting service eHermits, Inc.. So, every time an update comes out for WordPress, I can spend 5 seconds to grab the latest and all new accounts I create will be safe and updated.

Unlike an alias, this is done through a function call. Functions allow the use of multiple lines and variables. Here is the call I just added:

function upskel()
{
  cd /root/cpanel3-skel
  rm -R public_html
  rm latest.zip*
  wget http://wordpress.org/latest.zip
  unzip latest.zip
  mv wordpress public_html
}

Technically I probably could have done that as an alias but a function is much easier to read with multiple lines involved.

As a bonus, here is a function that takes a variable:

function  ewpc()
{
  cd /home/$1*/
  pwd
  sudo vi ./public_html/wp-config.php
}

Can you tell me what it does?

WordPress DB Hacks: Determining the ID of a category parents

When manipulating WordPress databases for exports and merges, sometimes it is helpful to get a list of all of the parents for the categories your posts are in.

For the import I am working on right now.  The category list is being flattened from 30 categories down to 5 categories. The blog has just over 75 thousand posts in it and doing the conversion manually would be a rather arduous process to say the least.

So, I came up with this little query to give me the parent category for each of the child categories on that site.

It’s fairly simple but I’ve had to write it several times now and thought maybe someone else might need it sometime too:

SELECT `tt1`.`term_taxonomy_id` as 'Child Tax ID', `t1`.`term_id` as 'Child Term ID', `t1`.`name` as 'Child Name', `tt2`.`term_taxonomy_id` as 'Parent Tax ID', t2.`term_id` as 'Parent Term ID', `t2`.`name` as 'Parent Name' FROM `wp_term_taxonomy` `tt1`, `wp_term_taxonomy` `tt2`, `wp_terms` `t1`, `wp_terms` `t2` where `tt1`.`term_id` = `t1`.`term_id` and `tt1`.`parent` = `t2`.`term_id` and `tt1`.`taxonomy` = 'category' and `tt2`.`term_id` = `t2`.`term_id` and `tt2`.taxonomy = 'category';

Now I can just grab it instead of rewriting it again next time.  After all, this is The Code Cave…

PS – If you just want to see the top level categories, here’s how:

SELECT `tt1`.`term_taxonomy_id`, `t1`.`term_id`, `t1`.`name` FROM `wp_term_taxonomy` `tt1`, `wp_terms` `t1`where `tt1`.`term_id` = `t1`.`term_id` and `tt1`.`taxonomy` = 'category' and `tt1`.`parent`=0

Why does Facebook keep telling me my email address is broken

Periodically I would get messages from Twitter and Facebook telling me that my email address is invalid. I would just hit reconfirm and it would work fine for a while.

When a client came to me and said he was getting the same messages, it became important to dig into it. What did I find? The Spam Cop is to blame.

Continue reading Why does Facebook keep telling me my email address is broken

Using find to copy specific files on Linux

I was faced with a weird copy command I wanted to do today; so I thought I would share.

How do you copy files in a directory tree to another directory?

I wanted to copy all of the mp3 files in a directory tree over to one specific target directory.

Initially I thought the command

cp -r *.mp3 /target/directory/

would work, but even though it specifies the –recursive option, it does not iterate the subdirectory looking for the mp3 files.

So I resorted to my every faithful companion: find -exec. It seems like this is one of the most useful tools in Linux. In this case, here is the command line I used:

find . -type f -name ‘*.mp3’ -exec cp {} /targetdirectory/ \;

How to: Find files edited in the last day

The process is straight forwarded. There are several methods:

find . -mtime -1 \! -type d -exec ls -l {} \;

Or more simply

find . -type f -mtime -1

In my case, I wanted to do more. First since I was searching an SVN repository, I wasted to exclude all of the extra files that are touched by SVN. Additionally, the goal was to show which files contained a print_r function.

Here’s what I came up with:

find . -path '*/.svn/*' -prune -o -type f -mtime -1 \
-exec echo '{}' \; -exec grep print_r {} \;

Hope that helps!

Why is AVG blocking legitimate sites?

AVG is a “free” antivirus software package that has become fairly popular lately. The b5media tech team has been asked many times in the last 24 hours about why AVG is blocking legitimate sites. There is a FAQ on AVG’s site about this, but it is incomplete and, in my opinion, inaccurate*. People are asking the questions in the AVG forum, but responses have just been long paragraphs explaining how they’ve asked the question in the wrong forum and links to the FAQ. I’ll attempt to better address the question here.

The FAQ says:

There are several possibilities how a clean and legitimate website may become infected:

  • Website was exploited by some hacktoolkit which searches for vulnerable websites, and automatically infects them.
  • Infection was inserted on the machine that is used to create/upload websites, which means that the author’s/administrator’s computer is infected.
  • An attacker gained direct access to the website administration thanks to a weak or stolen access password.

We recommend to contact the administrator of such website.

This may have largely been true 5 years ago, but it isn’t now. Yes, breaches in security happen and always will. However, the most common vector for malware to get on today’s sites is through ads.  These ads aren’t even ever seen by the webserver you are visiting**. 

The FAQ would be accurate if they added:

  • The website is currently displaying an ad/image hosted on a site that AVG has deemed dangerous.

The unprinted bullet point is:

  • We mess up. We are humans. We are not perfect and neither are the tools we use. Sometimes we will say a site is infected when it isn’t. Sometimes we will say something is malware, that really is, but it is so common place that blocking it will make so many sites unusable that we will have to back down. When this happens, we will try to “fix” the issue as quickly as possible.

This ad vector is something that all websites are fighting right now. It’s difficult because servers can be spotless and tight, but tools like AVG and the Google toolbar will see one of these ads (that we have nothing to do with) and will list that site as infected.  If a bad ad is served when Google scans for infection, then the site is completely dropped from the Google index. NOT GOOD. Then the admin has to go and figure out which ad is bad and from that which ad manager let something slip through that they should have blocked.***

So, what is the current issue? Well, on October 14, 2009 AVG reclassified an ad/cookie used on a large majority of websites out there as dangerous. That’s why AVG is blocking familiar sites such as http://nytimes.com, http://imageshack.us, http://yahoo.co.uk, http://babelfish.yahoo.com, http://problogger.com as well as other b5media entities like http://everyjoe.com http://splendicity.com and http://blisstree.com

Everything seems to point to ads.YieldManager.com as what is being blocked. Yieldmanager.com has an Alexa rating of 198. Think what you want of Alexa, 198 means they are BIG. You probably know the name as they have been on most sites you’ve visit for years.  YieldManager is run by Right Media, which, since 2007, has been controlled by Yahoo. This is why the ip addresses, that come up in the AVG LinkScanner alerts, all point to Yahoo.

In short, AVG will either change this decision or lose market share. It won’t take long for them to make up their mind.

UPDATE: Around 9am the forum included a post stating “UPDATE…. It’s now been ascertained that it was actually a false positive. Please update the AVG on your system.”

Forum users report that AVG has changed their statement from:

Let us inform you that this is not a false detection. Our LinkScanner
technology detects a real threat, which is Geo-IP and also browser
specific targeted, therefore it is detected only at www.yahoo.co.uk
(IP: ) in Mozilla Firefox web browser only.

Over to:

Unfortunately, the previous AVG Link scanner database might have
detected the mentioned web page as threat. However, after thorough
analysis we can confirm that it was a false alarm. We have released a
new Link scanner update that removes the false positive detection on
this web page. Please update your AVG and check if your are able to
open the web page properly.

As you can also see from that thread, some people received an update this morning, but still are having problems on popular sites.  My only suggestion is that you update AVG a few more times and hope that they allow you to surf your favorite sites.

 

 

 

* Yes, I’ve submitted feedback about this.

** The way most ads work is a webpage will include instructions for your computer to say “Hey, you big sexy ad server, give me an ad to display.” when the page is loaded.  At that point, the big sexy ad server says “Very well then. Go to ads.example.com and get the ad.”.  Your computer then visits ads.example.com which can return any number of different ads. Some will be perfect nice ads. Some may be nasty infectious ads. Others will work on one browser, and throw errors on another.  The site has no control over this. Really, neither does the big, sexy ad server (though it should run its own periodic checks) since the ad that appears at ads.example.com was probably legit when the ad was purchased. The ads that appear are only as reliably legit as the checks in place at that third party ad server.

*** That said, it is kinda interesting to look at all the sites out there listed as infected. For example go here to see an infected site and then bounce up from there into any of the 3 networks that site is hosted on. That will lead you to thousands of other infected listings.

Brain Storming on Blocking Bad-ads

I’m just jotting down some notes about using the Google Safe Browsing API to prevent a site from serving malicious/bad ads.

Problem Defined

  • Ads are put on a site via javascript by calls as simple as “getad(‘adposition1’)”. JavaScript is executed via the client’s browser after the page is served.
  • Those calls don’t touch any of our servers, they go from the client to the Google/Glam/Whatever Ad Server. So we don’t see the ads before they appear on the customer screens.
  • The ads being served may be malicious
    • Any ad that is served can link to a site that has been infected. We will want to block this.
    • Any ad that is served can “take over” the page and redirect the page to a site that may or may not have malware. We want to block ALL take over attempts.
    • There may be other types of ads that we wish to block.  Potentially we might wish to block specific ads on specific sites (i.e. a sexual connotations in ads on pre-teen audience sites). This may be beyond the initial scope and/or incur unwanted execution expenses.
  • Serving a malicious ad can get a site listed as “infected” even though your server has had nothing to do with ANY of the ad content.

Obstacles

  • Any extra calls WILL slow the page load process.
  • Each page load MUST call the ad serving script again
  • If an ad can be identified as bad, some other type of content must be served in that position to ensure page integrity.
  • The request for ad content HAS to come from the customer side because many ads are geo-specific and the customer’s IP determines what ad shows at what time.
  • You don’t want to set up a system where the site itself can submit a site as “bad” as anyone could sniff that info and seed our black list with bad data.
  • The results of the first getad() call could result in more javascript which must, in turn, be processed by the browser to produce the final ad. Potentially, several layers of JS could exist before the real ad is served. (e.g. 2 layers of indirection before ad: Google Ad Manager JS —serves—> Glam Ad embeded JS call —serves—> JS call to 3rd Party Ad Server —serves—> Ad). This pattern is real and happens often.

Possible solutions

  • Status Quo: As problem sites are reported to us, determine which ad is bad, report it to the ad server & hope they fix it before google sees it and lists the site as a dangerous site in it’s tool bar and in chrome.
    • Unless you are “lucky” you don’t get the badad.
    • Once you get the badad, it is hard to determine the initial JS that caused the problem
  • Embed everything JS with its own iframe
    • Will block take overs
    • May or may not prevent Google from listing the site, probably not.
    • Will break ads that are contextual based
  • Check the ad entirely on the client side via a black list: GSB API (http://code.google.com/apis/safebrowsing/) or PhishTank (http://data.phishtank.com/data/online-valid.xml)
    • This Good/Bad check could be done with a single call with the API call
    • Calls to external servers are dependent upon the health/bandwidth of that server
    • This could also be done via downloading the black list and checking off of that: http://code.google.com/p/jgooglesafebrowsing/wiki/Quick_Start_Guide
    • Blacklist downloading would cost time and would have to be updated periodically.
  • Implement a hybrid solution where a call is done to our servers to see if the an ad is good or bad.  (Server side base code: http://lampsecurity.org/php-google-safe-browsing-api )
    • Ad call is processed in JS eval (Will have to be checked for nested JS calls)
    • MD5 of ad is sent to the server. The results are Good/Bad/Unknown.  (Pass the url?)
    • If the result is Good, ad is served and process exits
    • If the result is Bad, either go to step 1, or serve place holder/known good ad & exit.
    • If the result is Unknown, send the JS to the server for verification. The server processes the code and returns a Good/Bad result.
    • If the result is Good, ad is served and process exits
    • If the result is Bad, either go to step 1, or serve place holder/known good ad & exit
  • Other solutions?

Reading

Anyway, I had this going through my head and wanted to get this all written out. So I can have a place to check back on this tomorrow…