1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 // This file contains the tests for detecting extension's ad injection in
6 // Chrome. This contains many different, independent tests, but it is all run
7 // as a "single" browser test. The reason for this is that we want to do many
8 // short tests for ad injection, and the set-up/tear-down time for a browsertest
9 // implementation of each would be prohibitive.
10 // See also chrome/browser/extensions/activity_log/ad_injection_browsertest.cc
12 // For all you data lovers, a few quick benchmarks.
13 // Taken on a HP Z620 Goobuntu Workstation.
14 // Method 1: Running all tests within a single browsertest, and cleaning up on
15 // the JS side after each (i.e., the method we're using).
16 // Method 2: Running so that every test has it's own browsertest.
18 // ----------------------------------------------------------
19 // Number of Tests | Method | Real | User | Sys |
20 // 1 | 1 | 6.002s | 2.850s | 0.580s |
21 // 10 | 1 | 8.387s | 3.610s | 0.920s |
22 // 100 | 1 | 33.765s | 11.740s | 3.000s |
23 // 1 | 2 | 6.020s | 2.940s | 0.470s |
24 // 10 | 2 | 57.945s | 26.580s | 4.950s |
25 // 100 | 2 | N/A | N/A | N/A |
26 // ----------------------------------------------------------
28 // This html block is just designed to be a massive playground for ad injectors,
29 // where they can let loose and have fun.
30 // We use this to populate our page at the start of every test.
32 '<iframe id="ad-iframe" src="http://www.known-ads.adnetwork"></iframe>' +
33 '<iframe id="non-ad-iframe" src="http://www.not-ads.adnetwork"></iframe>' +
34 '<a id="ad-anchor" href="http://www.known-ads.adnetwork"></a>' +
35 '<a id="non-ad-anchor" href="http://www.not-ads.adnetwork"></a>' +
36 '<div id="empty-div"></div>';
39 * The AdInjectorTest infrastructure. Basically, this allows the test to follow
40 * a simple iteration cycle:
41 * - Signal the C++ test that we're going to do page setup, and we shouldn't
42 * record any ad-injector events (as some of the page setup could qualify).
43 * - Do the page setup, which involves creating a 'playground' for ad injectors
44 * of iframes, etc. See also ad_injectors.html. We do this set up for each
45 * test, so that none of the tests conflicts with each other.
46 * - Signal that we are done with page setup, and should start recording events.
47 * - Run the next test.
48 * - Signal that the test is done, and we should check for the result.
50 * This cycle repeats, and should be done synchronously so that we don't end up
51 * recording events which we shouldn't, and can attribute each event to its
55 function AdInjectorTest(functions) {
57 * The list of functions to run in order to test ad injection.
58 * @type {Array.<Function>}
61 this.functions_ = functions;
64 AdInjectorTest.prototype = {
66 * The index of the function that is next in line.
73 * Start the page reset process. This includes sending a message to Chrome
74 * and verifying receipt (by waiting for a response, and then triggering the
78 resetPageAndRunNextFunction_: function() {
79 chrome.test.sendMessage('Page Reset Begin', function(response) {
80 this.resetPageAndRunNextFunctionImpl_();
85 * Reset the page. This means eliminating any previous "content" from the
86 * body, and refreshing it with a new, clean slate.
89 resetPageAndRunNextFunctionImpl_: function() {
90 document.body.innerHTML = kBodyHtml;
91 chrome.test.sendMessage('Page Reset End', function(response) {
92 this.runNextFunctionImpl_();
97 * Start the process to run the next function (which begins by resetting the
98 * page). If there is no next function, send the "Test Complete" message.
100 runNextFunction: function() {
101 if (this.currentFunction_ < this.functions_.length)
102 this.resetPageAndRunNextFunction_();
104 chrome.test.sendMessage('Test Complete');
108 * The implementation of runNextFunction().
111 runNextFunctionImpl_: function() {
112 var f = this.functions_[this.currentFunction_++];
115 // Try and catch any errors so one bad function does cause the whole suite
119 message = f.name + ':' + change;
121 message = 'Testing Error:' + f.name + ':' + e.message + ', ' + e.stack;
124 chrome.test.sendMessage(message, function(response) {
125 this.runNextFunction();
130 // Return values to signal the result of a test. Each test should return the
131 // appropriate value in order to indicate if an ad was injected, and, if so,
132 // what kind of injection.
133 // These match the enum values in
134 // chrome/browser/extensions/activity_log/activity_actions.h.
135 // The signal that there was no ad injection.
136 var NO_AD_INJECTION = 0;
137 // The signal that there was a new ad injected.
138 var INJECTION_NEW_AD = 1;
139 // The signal that an ad was removed.
140 var INJECTION_REMOVED_AD = 2;
141 // The signal that an ad was replaced.
142 var INJECTION_REPLACED_AD = 3;
143 // The signal that an ad was likely injected, but we didn't detect it fully.
144 var INJECTION_LIKELY_NEW_AD = 4;
145 // The signal that an ad was likely replaced, but we didn't detect it fully.
146 var INJECTION_LIKELY_REPLACED_AD = 5;
148 /* The "ad network" url to use for tests. */
149 var kAdNetwork = 'http://www.known-ads.adnetwork';
150 var kAdNetwork2 = 'http://www.also-known-ads.adnetwork';
152 /* The "non ad network" url to use for tests. */
153 var kMaybeAdNetwork = 'http://www.maybe-ads.adnetwork';
155 /* @return {?HTMLElement} The element with the given id. */
156 var $ = function(id) { return document.getElementById(id); }
158 // The following is a collection of functions to "get" an HTML ad. We need to do
159 // this for internal counting in testing, because if we simply do:
160 // var iframe = document.createElement('iframe');
161 // iframe.src = kAdNetwork;
162 // document.body.appendChild(iframe);
163 // We end up double counting ad injections. We count one for modifying the src
164 // of the iframe (on line 2) and a second for appending an iframe with an ad
165 // (on line 3). Instead of doing this, use the get*Ad() functions below. The
166 // example above would become:
167 // document.body.appendChild(getIframeAd());
169 // Creates an iframe ad, like so:
170 // <iframe src="http://www.known-ads.adnetwork"></iframe>
171 var kIframeAdTemplate = document.createElement('iframe');
172 kIframeAdTemplate.src = kAdNetwork;
175 * @return An iframe element which will count as ad injection in the tests.
177 var getIframeAd = function() {
178 return kIframeAdTemplate.cloneNode(true);
181 // Creates an anchor ad, like so:
182 // <a href="http://www.known-ads.adnetwork"></a>
183 var kAnchorAdTemplate = document.createElement('a');
184 kAnchorAdTemplate.href = kAdNetwork;
187 * @return An anchor ('a') element which will count as ad injection in the
190 var getAnchorAd = function() {
191 return kAnchorAdTemplate.cloneNode(true);
194 // Creates a script ad, like so:
195 // <script href="http://www.known-ads.adnetwork"></a>
196 var kScriptAdTemplate = document.createElement('script');
197 kScriptAdTemplate.src = kAdNetwork;
200 * @return A script element which will count as ad injection in the tests.
202 var getScriptAd = function() {
203 return kScriptAdTemplate.cloneNode(true);
206 // This series constructs a nested ad, which looks like this:
210 // <iframe src="http://www.known-ads.adnetwork"></iframe>
213 var div = document.createElement('div');
214 div.appendChild(document.createElement('span'));
215 div.appendChild(getIframeAd());
216 var kNestedAdTemplate = document.createElement('div');
217 kNestedAdTemplate.appendChild(div);
220 * @return A div with a nested element which will count as ad injection in the
223 var getNestedAd = function() {
224 return kNestedAdTemplate.cloneNode(true);
228 * The collection of functions to use for testing.
229 * In order to add a new test, simply append it to the collection of functions.
230 * All functions will be run in the test, and each will report its success or
231 * failure independently of the others.
232 * All test functions must be synchronous.
233 * @type {Array.<Function>}
237 // Add a bunch of elements, but nothing that looks like ad injection (no
238 // elements with an external source, no modifying existing sources).
239 functions.push(function NoAdInjection() {
240 var div = document.createElement('div');
241 var iframe = document.createElement('iframe');
242 var anchor = document.createElement('anchor');
243 var span = document.createElement('span');
244 span.textContent = 'Hello, world';
245 div.appendChild(iframe);
246 div.appendChild(anchor);
247 div.appendChild(span);
248 document.body.appendChild(div);
249 return NO_AD_INJECTION;
252 // Add a new iframe with an AdNetwork source by creating the element and
254 functions.push(function NewIframeAdNetwork() {
255 document.body.appendChild(getIframeAd());
256 return INJECTION_NEW_AD;
259 // Add a new iframe which does not serve ads. We should not record anything.
260 functions.push(function NewIframeLikelyAdNetwork() {
261 var frame = document.createElement('iframe');
262 document.body.appendChild(frame).src = kMaybeAdNetwork;
263 return INJECTION_LIKELY_NEW_AD;
266 // Modify an iframe which is currently in the DOM, switching the src to an
268 functions.push(function ModifyExistingIframeToAdNetwork1() {
269 var frame = $('non-ad-iframe');
270 frame.src = kAdNetwork;
271 return INJECTION_NEW_AD;
274 functions.push(function ModifyExistingIframeToAdNetwork2() {
275 var frame = $('non-ad-iframe');
276 frame.setAttribute('src', kAdNetwork);
277 return INJECTION_NEW_AD;
280 // Add a new anchor element which serves ads.
281 functions.push(function NewAnchorAd() {
282 document.body.appendChild(getAnchorAd());
283 return INJECTION_NEW_AD;
286 functions.push(function NewAnchorLikelyAd() {
287 var anchor = document.createElement('a');
288 document.body.appendChild(anchor).href = kMaybeAdNetwork;
289 return INJECTION_LIKELY_NEW_AD;
292 // Test that an extension adding an element is not considered likely ad
293 // injection if the element has a local resource.
294 functions.push(function LocalResourceNotConsideredAd() {
295 var anchor = document.createElement('a');
296 document.body.appendChild(anchor).href = chrome.extension.getURL('foo.html');
297 return NO_AD_INJECTION;
300 // Test that an extension adding an element with the same host as the current
301 // page is not considered ad injection.
302 functions.push(function SamePageUrlNotConsideredAd() {
303 var anchor = document.createElement('a');
304 // This source is something like 'http://127.0.0.1:49725/foo.html'.
305 document.body.appendChild(anchor).href = document.URL + 'foo.html';
306 return NO_AD_INJECTION;
309 functions.push(function ModifyExistingAnchorToAdNetwork1() {
310 var anchor = $('non-ad-anchor');
311 anchor.href = kAdNetwork;
312 return INJECTION_NEW_AD;
315 functions.push(function ModifyExistingAnchorToAdNetwork2() {
316 var anchor = $('non-ad-anchor');
317 anchor.setAttribute('href', kAdNetwork);
318 return INJECTION_NEW_AD;
321 // Add a new element which has a nested ad, to ensure we do a deep check of
322 // elements appended to the dom.
323 functions.push(function NewNestedAd() {
324 document.body.appendChild(getNestedAd());
325 return INJECTION_NEW_AD;
328 // Switch an existing iframe ad to a new ad network.
329 functions.push(function ReplaceIframeAd1() {
330 $('ad-iframe').src = kAdNetwork2;
331 return INJECTION_REPLACED_AD;
334 functions.push(function ReplaceIframeAd2() {
335 $('ad-iframe').setAttribute('src', kAdNetwork2);
336 return INJECTION_REPLACED_AD;
339 // Switch an existing anchor ad to a new ad network.
340 functions.push(function ReplaceAnchorAd1() {
341 $('ad-anchor').href = kAdNetwork2;
342 return INJECTION_REPLACED_AD;
345 functions.push(function ReplaceAnchorAd2() {
346 $('ad-anchor').setAttribute('href', kAdNetwork2);
347 return INJECTION_REPLACED_AD;
350 // Remove an existing iframe ad by setting it's src to a non-ad network.
351 functions.push(function RemoveAdBySettingSrc() {
352 $('ad-iframe').src = kMaybeAdNetwork;
353 return INJECTION_REMOVED_AD;
356 // Add an ad network script to the DOM.
357 functions.push(function AddNewScriptAd() {
358 document.body.appendChild(getScriptAd());
359 return INJECTION_NEW_AD;
362 // Add an ad network script by modifying the src attribute of a script element.
363 functions.push(function ModifyScriptSrcAttribute() {
364 document.body.appendChild(document.createElement('script')).src = kAdNetwork;
365 return INJECTION_NEW_AD;
368 // Ensure that we flag actions that look a lot like ad injection, even if we're
370 functions.push(function LikelyReplacedAd() {
371 // Switching from one valid url src to another valid url src is very
372 // suspicious behavior, and should be relatively rare. This helps us determine
373 // the effectiveness of our ad network recognition.
374 $('non-ad-iframe').src = 'http://www.thismightbeanadnetwork.ads';
375 return INJECTION_LIKELY_REPLACED_AD;
378 // Verify that we do not enter the javascript world when we check for ad
380 functions.push(function VerifyNoAccess() {
381 var frame = document.createElement('iframe');
382 frame.__defineGetter__('src', function() {
383 throw new Error('Forbidden access into javascript execution!');
386 return NO_AD_INJECTION;
389 // Test that using "eval" is not a way around detection.
390 functions.push(function EvalAdInjection() {
391 var code = 'document.body.appendChild(' +
392 ' document.createElement("iframe")).src = kAdNetwork;';
394 return INJECTION_NEW_AD;
397 // TODO(rdevlin.cronin): We are not covering every case yet. Fix this.
398 // See crbug.com/357204.
400 // Kick off the tests.
401 var test = new AdInjectorTest(functions);
402 test.runNextFunction();