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();