Bug Bounties in a Small Company

The E-mail arrived quietly in our support mailbox. Pretty good English, but clearly not the writer’s first language, and :

BUG : Password Reset Link Not Expire After Mail Change.

Hey!
I found a token miss configuration flaw in…

Ok. Not the biggest deal of all time, but something that should be fixed. The submitter provided really great instructions on how to reproduce the bug and why we should care. I replied thanking the submitter and got this back:

Hi There
Is there any way to give me a bounty ?

Thanks

Baboom. This sounds like the exact situation that Justin Jackson & Jon Buda asked about on this episode of Build your SaaS. We’re a small company. We don’t have a formal bug bounty or vulnerability disclosure program, but I find security fascinating, (at least from the outside), and know bug bounties are a thing. It would be nice to pay a bounty, but how much? What happens next? What if we refuse?

This is what we did, and how it played out.

It’s our first time

My business partner had seen the initial E-mail in the support system so he knew something was up. After a quick discussion about how bug bounties work and asking if he ok with paying something we made the decision to pay small bounties in the neighbourhood of $20 – $50 US per bug, depending on severity, available funds, and how many more reports came in.

I went back to the submitter and said that we could pay but as a very small company with an extremely limited budget he should not expect Apple-style bounties. They were happy with that arrangement, so I fixed the bug.

Then they submitted another but and I fixed that. And another. In all this person submitted 5 “bugs” of varying severity. Some we could barely class as problems but we looked at them all and patched any that needed patching. Some others made us re-think some of the user interface, especially around changing contact or login information, and make plans to change it later.

The reports were both submitted and fixed close together so we lumped everything into one payment. We paid about $100 US for the lot. The submitter asked for payment to go to a random PayPal address that was nothing like the name of the person we were dealing with, then send a screenshot of the completed payment. Pretty sketchy, but they were satisfied.

Writing a Disclosure Policy

I have looked at the bug bounty programs on HackerOne and Bugcrowd, and decided to make a page to point people to when they ask about bug bounties, and to lay down some ground rules, (like don’t DDoS us!). We used Mozilla’s Bug Bounty program and some others from respected companies as inspiration. We included a requirement for detailed reproduction steps, why the bug is actually a problem. This makes it relatively easy to triage any reports we get.

Good thing we wrote a disclosure policy, because as we were finishing up with the researcher another E-mailed us with some bugs, and asking if we have a bug bounty program, and we were was able to respond with a link to the new disclosure page.

Bounty Hunter #2

On Build your SaaS Jon asked if you paid, would you end up on a list of companies that pay, then be flooded with submissions. The answer seems to be at least sort of yes. As we were finishing up with the first submitter another showed up. Submitter #2 had better English than Submitter #1, and was much more thorough. I suspect these two were somehow connected, whether they both post to the same forum or both work for the same person I don’t know, but it felt like we went from a Level 1 pen-tester to Level 2. Submitter #2’s bugs were more creative and more serious than Submitter #1’s. Since #2 showed up just as we were finishing with #1 we were short on cash for bounties, but were honest about it and paid what we could. This seemed well received. I believe we paid about $200 US to Submitter #2.

After once we were mostly wrapped up with Submitter #2 things were quiet for a while, then he submitted one more issue about a week later which we fixed, and since we hadn’t sent the payment yet added to the payment for all of his bugs. Payment was the same sketchy unrelated PayPal + screenshot of the payment method.

Was it worth it?

Short term answer: Yes. At least one of the bugs that Submitter #2 found was serious enough that it warranted immediate attention, so on that level it was worth having someone report it instead of exploit it.

Overall answer: Still yes. For the cost of about $300 US and about a week of my mornings we got outside feedback that made us reconsider some UI decisions from a security standpoint and learned about Content Security Policies and other security headers. Also, the submitted bugs and our knowledge of our systems led us to other related bugs that we fixed as well, so it was a bit like a 2-for-one sale on bug-fixes. And now we have tests written that should prevent these bugs from re-appearing.

Of course I would prefer that we found problems internally before they were deployed, and problems would be discovered at convenient times, but that’s not the real world and I’m happy the problems were found and fixed, and we improved our systems and knowledge as a result.

This all happened back in February and March, and we haven’t had any reports since.

Further Steps

I hear there’s an ISO standard for security disclosure best practices, (apparently ISO 29147 and ISO 30111). I plan on looking them up and do what we can to follow them while balancing that with continuing to improve our product. With a small team it’s always a question of balance, so we’ll keep doing our best!

Has this happened to you?

If I had to bet, I would bet that a lot of small businesses are approached by security researchers this way, but none of us have dealt with them before, we don’t know what to do, and are worried it will be a scam or that we’ll end up on a list and end up spending all our time dealing with bug reports. This feels like something we should talk about.

Local Commerce

Justin Jackson lays out the case for turning all of our favourite local stores into a viable Amazon competitor in his post Fight Amazon. It got me thinking about the problems I’ve had trying to buy things from local stores over the past year, and maybe some solutions.

Justin musing about how local stores can compete with Amazon.

Local Commerce Problems

I try to support local stores when I can. I don’t usually buy clothes, (somehow I’m still clothed), so I can’t speak knowledgeably about clothing shopping, but there have been some instances over the past year when I’ve been incredibly frustrated with local shopping.

It’s the bike shoes that the bike shop had to order twice and took a month to get, late in the season, or the bike brake levers that the bike shop ordered the model for the wrong kind of brake, or the indoor soccer shoes that, even after checking the Sport Chek website were not in stock, or when our portable washing machine broke down an there was no way to buy one locally without actually visiting or calling all the appliance stores to check their stock. Don’t even get me started on trying to buy several yards of cloth when someone in the family is making costumes for a dance recital.

In all of these cases a service that could tell me where products are in stock locally would have solved my problem. Actual purchasing and delivery would make things even better for some products.

The Inventory Problem

Where do I go to buy this?

For stores that already have E-Commerce systems in place it’s probably not too hard to answer this question. The problem is making sure that the E-Commerce systems exist, and have accurate inventory numbers. I haven’t worked in retail for a long time, and when I did we weren’t using anything computerized to record inventory. These days I think some, (but not all!), stores to have computerized inventory but making the systems used in stores talk to E-Commerce systems may be difficult.

For stores that already have reliable stock levels in their E-Commerce systems it should be relatively simple to know their inventory numbers using something like a Google Product Feed. That could be used either by other local stores who are willing to refer customers to competitors with products in stock, or by an aggregator that provides a local shopping experience.

Shipping

I should also mention shipping: It’s expensive, and it’ll be tough to compete against Amazon, but there’s probably a way to make it happen, and it’s not my area of expertise so I don’t know if any of my ideas are realistic.

The Business Problem

If the technical side works, and we have a bunch of local stores that can take orders online, ship easily and inexpensively, and find alternative sources for items they don’t have, and we have an aggregator where people can go to shop instead of Amazon, we still have a business problem, or two:

  1. Why would a store recommend that a customer go buy from their competitor now instead of trying to take the order themselves and order in the product?
    • Justin mentions commissions in his video. Why would a store pay a commission to its competitor instead of trying to get the sale itself some other way?
  2. How does the aggregator make money without becoming a pay-to-play advertising service?
    • Maybe commissions here too, but is it possible to get enough store owners on board with paying the commission instead of taking their chances with Adwords?

Next?

I doubt that I’ll be the person to try to solve these problems, but someone should.

To get to the level of a real, local-everywhere Amazon alternative let’s start with some technical questions.

Technical Questions:

  • Do stores even use computers to track stock?
    • Does that change if they sell online?
  • How hard is it to make in-store stock-tracking software talk to E-Commerce software?
    • What is even being used to track stock in stores?
    • Does it have APIs? Can it send notifications to other systems when stock levels change, or receive notifications to change its internal level when an E-Commerce order is received?
    • Do we have a good way of identifying the same product across multiple stores? I know that GTINs exist, but suspect they’re less than perfect.

If you know the answers to these, or have more questions, post in the comments like it’s 2005, or we can talk about it on Twitter.

Slashlocal: Someone is already trying

A parent I know from my kids school runs Slashlocal, which is trying to solve some of the local problems. I downloaded it today and will see if it helps me solve their problems. I’m worried their pricing model will discourage stores from putting their entire inventory online, but I believe the product is young they’re working hard to help on-board customers at no cost during the current crisis, so maybe changes are coming.

Update October 2020: I see Slashlocal now has a “Stripe fees + 1%” pricing option with a high limit on the number of items stores can put online. This sounds like a good deal.

Maybe I’ll build a WooCommerce extension?

If you’re interested in a WooCommerce extension to show other places to buy an item when your store is out of stock or you don’t carry what the customer is looking for let me know. If there’s enough interest I’ll build it.

DeadTrees 1.1: More Cover Sources

An update to DeadTrees, my WordPress plugin for sharing the books I read, is now live in the WordPress Plugin Repository. This update does one major thing: diversifies the sources of cover images.

What happened was Amazon introduced a quota on the API that the original DeadTrees was using to fetch cover art. The API quota is based on how many sales a user has, and I’m famous enough to have many sales, so my API access was eventually cut off. When I started posting my backlog of books I really wanted cover art, so I polished off the (very) dusty code and got to work.

Version 1.1 of DeadTrees maintains support for Amazon cover art and adds support for fetching cover art from OpenLibrary.org and LibraryThing. There’s a setting to try Amazon first or last, and the plugin tries to be smart about when to try OpenLibrary.org or LibraryThing, (it prefers OpenLibrary.org, as they seem to provide larger images, and don’t require an API key).

If you want to see DeadTrees in action take a look at the list of books I have read, which is powered by DeadTrees. For support post in the WordPress.org forum, and for bugs & feature requests post in the same forum or create a Github issue.

Happy Reading!

Adding Google Ads and Microsoft Advertising Conversion Tags to WooCommerce

Adding Google Ads and Microsoft Advertising conversion tracking tags to WooCommerce powered E-commerce shops can seem confusing at first: WooCommerce templates are like a huge Russian nesting doll and it’s unclear where to put the tags. On top of that Google wants their tags as close to the top of the document as possible, but many tutorials suggest using the woocommerce_thankyou hook, which dumps its output right in the middle of the body.

Use Any WordPress Hook

Luckily we can just use WordPress hooks to insert conversion tags where we need them, so long as we make sure we’re on the “Thanks for your order” page, and check that the order is a status that we want to count as a conversion. Here’s how we can do that:

/**
 * This function can be hooked onto any normal WordPress hook. Maybe wp_head,
 * or wp_footer, or even a sidebar if you really want.
 * @return [type] [description]
 */
function jb_do_stuff_on_order_complete_page() {
	// is_order_received_page() is part of WooCommerce
	// if there's a possibility that the page is loaded without WooCommerce
	// you could check to make sure it exists:
	if(!function_exists('is_order_received_page')) return;

	if(is_order_received_page()) {
		global $wp;
		$order = wc_get_order($wp->query_vars['order-received']);
		if($order && !$order->has_status( 'failed' )) {
			// Do what we need here.
		}
	}	
}

Set Up Conversion Tracking

Both Google and Microsoft have two parts to their tracking tag: a global tag that goes on every page of the website, (Microsoft calls this a UET tag, for “Universal Event Tracking“), and a conversion snippet. Both companies ask that that the global tag appears first in the document’s source. For Google this is because the gtag() function used by the conversion snippet is defined by the global tag.

Google Ads

Google really likes their global tag to be as high in the page’s source as possible – right after the opening <head> tag if possible. Because of this I often put the global tag right into header.php. A more portable solution is to use the wp_head hook so we’ll do that here:


function jb_set_up_google_tags() {

	// Start with the global tag.
?>
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-XXXXXXX-1"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'UA-XXXXXXX-1');	// sets up Google Analytics - Google will give you the exact value
  gtag('config', 'AW-XXXXXXX');		// added for the conversion - Google will give you the exact value

  if (window.performance) {
    var timeSincePageLoad = Math.round(performance.now());
    gtag('event', 'timing_complete', {
      'name': 'load',
      'value': timeSincePageLoad,
      'event_category': 'JS Dependencies'
    });
  }
</script>
<?php

	// Add the conversion tag IF this is an Order Complete page.
	if(is_order_received_page()) {
		global $wp;
		$order = wc_get_order($wp->query_vars['order-received']);
		if($order && !$order->has_status( 'failed' )) {
?>		
<!-- Event snippet for Sales conversion page -->
<script>
  gtag('event', 'conversion', {
      'send_to': 'AW-XXXXXXX/XXXXXXXXX__XXXXX',		// Google will provide this value.
      'value': <?php echo $order->get_subtotal(); ?>,
      'currency': 'USD',							// Change you deal with a different currency. Or set it dynamically.
      'transaction_id': '<?php echo $order->get_order_number(); ?>'
  }); 
</script>
<?php
		}
	}
}

// Set priority 1 to appear as soon as possible in the wp_head.
add_action( 'wp_head', 'jb_set_up_google_tags', 1 );

Boom! That’s it for Google.

Microsoft Advertising

Microsoft advertising is very similar, except they seem to suggest that they want the conversion snippet before the closing </body> tag, and don’t seem to mind about the UET tag, so let’s just put them both in the wp_footer:

function jb_add_microsoft_advertising_tags() {

	// Start with the UET tag that goes on every page.
?>
<script>(function(w,d,t,r,u){var f,n,i;w[u]=w[u]||[],f=function(){var o={ti:"XXXXXXX"};o.q=w[u],w[u]=new UET(o),w[u].push("pageLoad")},n=d.createElement(t),n.src=r,n.async=1,n.onload=n.onreadystatechange=function(){var s=this.readyState;s&&s!=="loaded"&&s!=="complete"||(f(),n.onload=n.onreadystatechange=null)},i=d.getElementsByTagName(t)[0],i.parentNode.insertBefore(n,i)})(window,document,"script","//bat.bing.com/bat.js","uetq");</script>
<?php

	// Then add the conversion tag IF this is an Order Complete page.
	if(is_order_received_page()) {
		global $wp;
		$order = wc_get_order($wp->query_vars['order-received']);
		if($order && !$order->has_status( 'failed' )) {

			$order_info = [
				'revenue_value' => $order->get_subtotal(),
				'currency' => 'USD'
			];
?>
<script>
   window.uetq = window.uetq || [];
   window.uetq.push('event', '', <?php echo json_encode($order_info); ?>);  
</script>
<?php
		}
	} // end if this is an order received page.		

}
add_action( 'wp_footer', 'jb_add_microsoft_advertising_tags', 90 ); // Priority 90 to appear near the end.

Microsoft’s documentation on adding the conversion snippet is somewhat confusing. It talks about adding extra Javascript functions to supply the conversion value and doesn’t say what data type the conversion value should be, (let me know if you know). A Javascript function to extract the conversion value from the DOM might be a good idea when there’s no server-side access to the purchase data, but if you’re developing a WooCommerce theme or plugin you have the access you need to just print the conversion value in the tag with PHP, (as we did above).

Conclusion

That’s it. I heard that some people have trouble getting Bing working, (probably because of the extra-confusing directions from Microsoft), and I had trouble finding resources on adding conversion tags to the header for Google Ads. Hopefully this helps people with one, (or both), of the same questions.

Remove a model’s Global Scopes in Laravel Nova

I am working on a Laravel project that has moderated reviews. Most of the time we only deal with reviews that have been approved, so the Review model has an ApprovedScope Global Scope to only show approved review, but staff members need to see un-approved reviews in our Nova admin so that we can approve, (or reject), reviews. Removing a Global Scope from a model for all of Nova is trickier than it appears at first glance, but there’s at least one way to do it, and maybe more.

Things that do not work

Call withoutGlobalScopes() in the Nova::serving() callback

Googling “remove global scope nova” turns up a post on Medium called Add or remove global scopes in Nova. Sounds promising! It claims we can remove global scopes by adding this to the boot method of the `NovaServiceProvider:

class NovaServiceProvider extends NovaApplicationServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Nova::serving(function () {
            \App\Rating::withoutGlobalScopes();
        });
    }
}

Cool! But it doesn’t work. When adding a global scope to a model using the model’s boot method like the Laravel documentation shows, the model’s boot method is called after the Nova::serving() callback. So in this situation we remove nothing from the Rating, then add a global scope later when the model is booted.

Call withoutGlobalScopes() in the model’s booted event

If a model has a global scope added during its boot then we should be able to remove it during the ‘booted’ event – or at least that’s what I thought, so I tried this in NovaServiceProvider::boot :

Nova::serving(function() {
    Event::listen('eloquent.booted: App\Rating', function($rating) {
        Rating::withoutGlobalScopes();
    });
}); 

No dice. I’m not certain why this doesn’t work, but some XDebug spelunking shows me that:

  1. There’s no withoutGlobalScopes() method on Laravel models, but when the model gets a method call that it doesn’t recognize it passes it to the model’s Builder instance.
  2. The Builder does have a withoutGlobalScopes method, so we don’t get an InvalidMethodException. In the Builder’s withoutGlobalScopes call the ApprovedScope is removed from the Builder’s list of global scopes.
  3. But the Global Scope is still applied to the query. My best guess is that a different query builder is used to actually generate results, or that later, when the models are about to be retrieved from the DB, the model re-passes the list of Global Scopes to the builder – and since we haven’t removed the Global Scope from the model it gets re-added to the Query Builder. If anyone knows what’s actually going on here I would love to know in the comments or on Twitter.

Something that does work

We need to remove the Global Scope after the model has booted and prevent it from being passed or re-passed to the Query Builder. How about a Model::withoutGlobalScopes method? The model’s addGlobalScope method comes from the Illuminate\Database\Eloquent\Concerns\HasGlobalScopes trait, and stores the global scopes in a static::$globalScopes[static::class] array. Creating our own HasRemovableGlobalScopes trait, with withoutGlobalScope and withoutGlobalScopes methods that mirror the signature of the Illuminate\Database\Eloquent\Builder withoutGlobalScope and withoutGlobalScopes methods can solve the problem, (also available as a Gist):

<?php

namespace App\Concerns;

use Closure;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Support\Arr;

trait HasRemovableGlobalScopes {

	/**
	 * @param  \Illuminate\Database\Eloquent\Scope|string  $scope
	 */
	public static function withoutGlobalScope( $scope )
	{
		if (is_string($scope) && is_array(static::$globalScopes[static::class])) {
			Arr::forget(static::$globalScopes[static::class], $scope);
		} elseif ($scope instanceof Closure) {
			Arr::forget(static::$globalScopes[static::class], spl_object_hash($scope));
		} elseif ($scope instanceof Scope) {
			Arr::forget(static::$globalScopes[static::class], get_class($scope));
		}
	}

	/**
	 * @param \Illuminate\Database\Eloquent\Scope[]|string[] $scopes
	 */
	public static function withoutGlobalScopes( array $scopes = [])
	{
		if(empty($scopes)) {
			static::$globalScopes = [];
		} else {
			foreach($scopes as $scope) {
				static::withoutGlobalScope($scope);
			}
		}
	}
}

The withoutGlobalScope method mirrors the HasGlobalScopes::addGlobalScope method to remove a single global scope, and the withoutGlobalScopes method can accept an array of global scopes to remove or be called with no parameters to remove all global scopes, (the same as Builder::withoutGlobalScopes).

A drawback?

The fact that we have to do this to remove global scopes for Nova seems to be an oversight. I’m hopeful that withoutGlobalScope/withoutGlobalScopes methods will be added to future versions of Laravel. If that happens there will be a method name collision between the HasRemovableGlobalScopes trait’s methods and the first-party ones, so read the release notes if you’re going to use this method.

Something else that might work

After writing the HasRemovableGlobalScopes trait I realized it should be possible to create an additional global scope that undoes my original global scope. In this case something like this might work:

Nova::serving(function() {
    Event::listen('eloquent.booted: App\Rating', function($rating) {
        Rating::addGlobalScope(new AllRatingsScope);
    });
}); 

Where AllRatingsScope says to include all ratings. I haven’t tried this so don’t know what happens when there are two global scopes that specify opposite conditions. You might get no results, or the scope that’s applied last might win. Assuming that the scope applied last wins it’s still important to use the eloquent.booted event to make sure the “undo” scope is added after the original scope.

All together now

I’m sticking with the HasRemovableGlobalScopes trait. I feel like actually removing the Global Scope is more logical than adding another scope to undo what the first one does. Using the HasRemovableGlobalScopes trait, this is what my whole system looks like:

Apply the original global scope to the Rating, (or wherever you need a global scope), and use the HasRemovableGlobalScopes trait:

class Rating extends Model
{
	use HasRemovableGlobalScopes;

	// ... 

	protected static function boot()
	{
		parent::boot();
		static::addGlobalScope(new ApprovedScope);
	}

	// ...
}

Set the event listener in the NovaServiceProvider’s boot method to remove the global scope for Nova:

class NovaServiceProvider extends NovaApplicationServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
	    Nova::serving(function() {

		    Event::listen('eloquent.booted: App\Rating', function($rating) {
			    Log::info('rating booted ' . request()->url());
		    	Rating::withoutGlobalScopes();
	        });
	    });

	    parent::boot();
    }
}

It works well for me. A possible enhancement would be to have my own App\Model class that extends Laravel’s Model and uses HasRemovableGlobalScopes, then extend my other models from that. At the moment I don’t have my own App\Model, and don’t have a lot of places where I need to share between all my models, but if I find I’m sprinkling too many use HasRemovableGlobalScopes lines around my code base I’ll make the change.