Closed Bug 86984 Opened 23 years ago Closed 23 years ago

trying to access history.length gives "access denied"

Categories

(Core :: Security, defect, P1)

x86
Windows 2000
defect

Tracking

()

VERIFIED FIXED
mozilla0.9.4

People

(Reporter: kchen, Assigned: security-bugs)

References

()

Details

(Whiteboard: patch)

Attachments

(1 file)

I have a frameset page with 3 frames in it, frame0, frame1, and frame2.  FRame0 
is for header page.  Frame1 is for text page, and frme2 is navigation page 
which contains three buttons, "previous", "next", or "cancel".

After frame1 is loaded, it will load frame2 with another navigation page.  
When the "next" button on frame2 is clicked, frame1 signal frame1 to submit.
when the "previous" button on frame2 is licked, frrame2 signal frame1 to go 
back history and then it execute parent.frames[2].history.back();  frame1 
constantly check if "previous" button in frame2 is clicked(the signal).  If it 
is clicked, frame1 execute parent.frames[1].history.back().  Therefore, click 
the "previous" button at frame2 let frame1 and frame2 both go back to their 
previous pages.  

This Works in NN 4.7 and IE5.

In the build 2001060703, If I click "next", then "previous", then "next" and 
then "previous", frame1 first loads a new page and it then load frame2 a new 
page after first "next", then frame1 and  frame2 both load the previos history 
pages after first "previous", then frame1 loads a new page and then loads 
frame2 a new page after second "next", but frame1 and frame2 do not load their 
history pages after second "previous".  When I check the history of frame2 by 
parent.alert("history=" + parent.frames[2].history.length), I got "Error: 
uncaught exception : Permission denied to access property".  If I check the 
history.length using Netsape 6.01, I got history.length = -1.  


The following are code for frames and pages.

//-----------------in frames.asp


<html><head>
<title>Expandable Web Development</title>
</head>
<frameset  rows="44, *, 78" frameborder=0  framespacing=0 noresize 
scrolling="no" cols="*" border=0>
	<frame marginwidth="0"  marginheight="0" src="Header.asp" 
name="heading" noresize scrolling="no">
	<frame marginwidth="10" marginheight="0" src="page1.asp" 
name="maintext">
	<frame marginwidth="0"  marginheight="0" src="Navigation.asp?
prevs=Prev&nexts=Next&closes=Cancel" noresize scrolling="no">
</frameset>
</html>


//----------------in Header.asp

<HTML><HEAD><TITLE></TITLE></HEAD>
<body bgcolor=blue>
</body></html>


//----------------in page1.asp
<%@ Language=JavaScript %>

<!--#include file="exi.button.asp"-->


<HTML><HEAD><TITLE></TITLE>   
</HEAD>
<body onLoad="change_button(); IntervalID = setInterval('button_clicked()', 
250);">
<form action="page2.asp" method=post id=form1 name=form1><h1> Page1</h1>
		<input name=prevs type=hidden value="none">
		<input name=nexts type=hidden value="Next2">
		<input name=closes type=hidden value="Cancel">	
</form> 
</body>  
</html> 


//-----------------in page2.asp

<%@ Language=JavaScript %>

<!--#include file="exi.button.asp"-->

<HTML><HEAD><TITLE></TITLE>   
</HEAD>
<body onLoad="change_button(); IntervalID = setInterval('button_clicked()', 
250);">
<form action="page2.asp" method=post id=form1 name=form1><h1> Page2</h1>
		<input name=prevs type=hidden value="prev1">
		<input name=nexts type=hidden value="Next3">
		<input name=closes type=hidden value="Cancel">	
</form> 
</body>  
</html> 


//----------- in include file exi.button.asp

<script LANGUAGE="JavaScript">
<!--
	 
	var npage= false; 
	var submit_clicked = 0; 
	var back_clicked = 0; 
	var IntervalID; 
	
	function button_clicked(){ 
		if (submit_clicked != 0) {
			document.forms[0].submit(); 
			submit_clicked = 0;
		}						
		if (back_clicked != 0) { 
			//parent.alert("history=" + parent.frames
[1].history.length);
			parent.frames[1].history.back();	
			back_clicked = 0;	
		}							
	} 
	 
	function change_button() { 
		//parent.alert("start change");
		var s = "Navigation.asp"; 
		if (document.forms[0].prevs.value) 
			if (document.forms[0].prevs.value != "none") { 
				if (s.indexOf("?") > 0) 
					s += "&prevs=" + document.forms
[0].prevs.value; 
				else 
					s += "?prevs=" + document.forms
[0].prevs.value; 
			} 
		if (document.forms[0].nexts.value) 
			if (document.forms[0].nexts.value != "none") {  
				if (s.indexOf("?") > 0) 
					s += "&nexts=" + document.forms
[0].nexts.value; 
				else 
					s += "?nexts=" + document.forms
[0].nexts.value; 
			} 
		if (document.forms[0].closes.value) 
			if (document.forms[0].closes.value != "none") { 
				if (s.indexOf("?") > 0) 
					s += "&closes=" + document.forms
[0].closes.value; 
				else 
					s += "?closes=" + document.forms
[0].closes.value; 
			} 
		parent.frames[2].location = s; 
	} 
//-->
</script>



//----------------- in Navigation.asp

<%@ Language=JavaScript %>
<html>
<script language="JavaScript">


var prevs; 
var nexts; 
var closes;

function go_backward() {
	if (parent.frames[1].back_clicked == 0) {
		parent.frames[1].back_clicked = 1;
		parent.alert("history=" + parent.frames[2].history.length);
		parent.frames[2].history.back();
	}
}

function go_forward() {
	if (parent.frames[1].submit_clicked == 0) {
		parent.frames[1].submit_clicked = 1;
		if (parent.frames[1].document.forms[0] != null)
			if (parent.frames[1].document.forms[0].nextpage != null)
				parent.frames[1].document.forms
[0].nextpage.value = "";
	}
}

function close_windows() {
	parent.close();
}

</script>

<body background="images/bksample2.jpg">
<form name="navibar" onSubmit="return false;">
<%	
	if (Request.QueryString("closes").Count != 0) {
%>
	<table width=100%>
		<td align=center>
		<%	 
			
			if (Request.QueryString("prevs").Count != 0) {
				prevs = Request.QueryString("prevs").Item;
				Response.Write('<input type="button" value="' + 
prevs + '" style="font-weight:bold; " onClick="go_backward(); return false;" 
id=button1 name=button1>');
			}
			if (Request.QueryString("nexts").Count != 0) {
				nexts = Request.QueryString("nexts").Item;
				Response.Write('<input type="button" value="' + 
nexts + '" style="font-weight:bold; " onClick="go_forward();" id=button2 
name=button2>'); 
			}
			if (Request.QueryString("closes").Count != 0) {
				closes = Request.QueryString("closes").Item;
				Response.Write('<input type="button" value="' + 
closes + '" style="font-weight:bold; " onClick="close_windows();" id=button3 
name=button3>'); 
			}
		%>
		</td>
	</table>
<% 
	}
%>
</form>
</body>
</html>
-> security
Assignee: asa → mstoltz
Component: Browser-General → Security: General
QA Contact: doronr → ckritzer
Wow, that was a lot of description, and quite confusing. I believe this boils
down to being able to read the value of history.length. We've recently made the
history.length function inaccessible to untrusted Javascript. We assumed there
was no legitimate reason for a script to read history.length. Can you do what
you need to do without using history.length? If not, I'll consider making it
accessible again. I know it's accessible in NS4 and IE5.

If you're having problems besides history.length, could you file those in a
separate bug and attach a testcase? Your description is difficult to follow.

Please let me know if you really need history.length. Otherwise this is a WONTFIX.
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Priority: -- → P4
Target Milestone: --- → mozilla0.9.3
*** Bug 87465 has been marked as a duplicate of this bug. ***
Summary: got error : uncaught exception: Permission deneied to access property. → trying to access history.length gives "access denied"
I don't really think this should be a wontfix if it's easy enough to fix; 
etour.com uses this all of their site.
History.length gives information about how long you've been browsing and/or how
you browse.  It would be nice to continue preventing web sites from accessing
this  information.
Blake et al,
  How many sites out there use history.length? I realize it's a de facto
standard to allow access to that one. I'm thinking about allowing access to that
one again. How great a privacy risk is posed by history.length? Yes, it gives a
little information about your browsing habits, but is it much more than a site
could get from its server logs?
> How many sites out there use history.length? 

So, I can't answer that one, but I can give you one very high-profile site that 
depends on (at least) not aborting the script due to an attempt to access that 
property -- travelocity.com. Go to www.travelocity.com and click on Flights or 
Cars/Rail; the javascript console will show "uncaught exception: Permission 
denied to access property". This aborts the top level script; I'm not sure how 
much is broken (the popup calendars are one thing), but I'd rather not find out
later about how you can't do 'X' on Travelocity (and others?) with Netscape's 
latest browser release.

I vote for allowing legacy access to this property (i.e., do what Nav4.x did, 
even if it was in error). I don't see this as a substantive privacy issue.
Hmmmnn....

I see the points of view from Jesse, Mitch and JRGM's comments, and I'm inclined 
to move we re-enable the ability to view history.length, since it returns only 
an integer indicating how many websites you have in your history, and *nothing 
else*1.  

The one thing I'm wondering is:  If we keep things as they are, do we have a 
simple work-around to offer to websites such as http://www.travelocity.com/ and 
others to avoid (effectively) killing their scripts?

Bottom line:  My vote is we re-enable the ability to view history.length, and 
cite legacy with NS4.x & IE.

1. Bad Monty Python reference
I would really like to see this change landed on the RTM branch. I don't 
believe there is a compelling privacy issue, and this flat out breaks de
facto compatibility with other implementations (even if exposing this 
property occurred as an oversight at some point in the past).

I assume the change is as simple as :

Index: all.js
===================================================================
RCS file: /cvsroot/mozilla/modules/libpref/src/init/all.js,v
retrieving revision 3.251
diff -u -r3.251 all.js
--- all.js      2001/07/03 01:05:00     3.251
+++ all.js      2001/07/15 05:14:26
@@ -174,7 +174,7 @@
 pref("capability.policy.default.History.forward", "allAccess");
 pref("capability.policy.default.History.go", "allAccess");
 pref("capability.policy.default.History.item", "UniversalBrowserRead");
-pref("capability.policy.default.History.length", "UniversalBrowserRead");
+pref("capability.policy.default.History.length", "allAccess");
 pref("capability.policy.default.History.next", "UniversalBrowserRead");
 pref("capability.policy.default.History.previous", "UniversalBrowserRead");
 pref("capability.policy.default.History.toString", "UniversalBrowserRead");

So, that's 
 (1) a "zero" risk patch, 
 (2) maintains backward compatibility with the de facto DOM 0 standard, 
 (3) avoids breaking a top100 site, 
 (4) exposes nothing that has not been exposed by previous shipping versions
     of Navigator 4.x, and that value is the number of URLs previously loaded
     in the current window, which in practice is pretty much a random number.
... and note that setting that to 'allAccess' does not open the ability to 
set that property since it is 'readonly attribute nsIDOMHistory history;'

To answer ckritzer's question: we could return '0' to all getters instead 
of throwing an exception, but that would require extra C++ code to implement
and hence risk. 

The only "workaround" as a JS author would be to add some kludge like
    var len;    
    if (parseInt(navigator.appVersion) > 4) { //XXX need better sniff
        eval("try { len = history.length; } catch(e) { len = 0; }");
    } else {
        len = history.length;
    }
to existing scripts, but that's not practical.
This change landed 5/8/01 in jst's XPCDOM landing. cc'ing jst for input on why.
I don't know why this was changed, but I see very little reason for not exposing
history.length to scripts, and since the urls of huge sites are mentioned here I
don't think disabling access to something that's not protected in 4.x and IE is
worth it. We should get this into the RTM branch!

sr=jst for John's fix.

Nisheeth, nsBranch?
Keywords: mozilla0.9.3
Opening history.length would not just allow web sites to figure out your
browsing habits.  It would also allow sites to find out whether you have an
account at any site that redirects from a "logged-in" URL to a "please log in"
page when you're not logged in.

Note: making history.back() allAccess rather than sameOrigin, and allowing
history.go(-2) to jump over URLs at another domain, both lead to the same
privacy hole as opening history.length.

>The only "workaround" as a JS author would be to add some kludge like...

Or they could avoid using history.length when it isn't necessary.

By the way, an alternate solution to the history.length privacy problem that
might not break as many sites: make history.length only report the number of
contiguous history entires *at the same domain* unless the script has the
UniversalBrowserRead privilege.
Jesse sez:

"Opening history.length would not just allow web sites to figure out your
browsing habits.  It would also allow sites to find out whether you have an
account at any site that redirects from a "logged-in" URL to a "please log in"
page when you're not logged in."

???  I'm confused by both these statements.  Could you provide an example 
(either a script attachment or text in the bug) of how this is possible?  The 
reason I ask is because I'm not seeing how you could do this when all 
history.length returns is an integer - I'm sure I'm missing something here.
For the record, 'history.length' was readable in Navigator 2.02, which was in 
public beta 6 years ago. 
Keywords: nsBranch
Here's how opening history.length would allow sites to find out whether you have
an account at a site that redirects slowly from a "logged-in" URL to a "please
log in" page when you're not logged in:
1. Open http://www.paysite.com/members in a frame.
2. Wait 10 seconds (because the redirect at paysite.com is 5 seconds).
3. In the frame, set location.href to a URL whose hostname is the same as the
attacker's.
4. Wait 3 seconds so the page can load.
5. Ask the frame for its history.length.

If history.length is 2, no redirect (or a fast redirect) occured; if
history.length is 3, a slow redirect occurred.
Travelocity is broken. This one-line all.js file fix takes care of it. 
N4.X, Opera and IE give access to history.length.  This makes us "common
denominator" rather than different.
PDT+ as per Clayton.
Whiteboard: [pdt+]
> -pref("capability.policy.default.History.length", "UniversalBrowserRead");
> +pref("capability.policy.default.History.length", "allAccess");

If we need to do this for 6.1, let's just remove the line and let it default to
sameOrigin, instead of making it allAccess.  It's good practice to limit
allAccess to things that really need to be allAccess.

Internet Explorer doesn't even make window.history allAccess, so we won't break
IE-compatible sites by only letting scripts access history.length from windows
they own.  I tested Travelocity with the line removed from my all.js and it
works fine.
Sure, just remove it if that is a better way to allow access to history.length.

By the way, is Mitch checking this in? (I don't have access). 

Checked into the branch using jst's cvs account.

Here are some possibilities for the trunk:

1. Use the same fix on the trunk that was used on the branch.
Advantages:    doesn't break any sites.
Disadvantages: leads to several privacy holes.

2. Make history.length only count contiguous sites at the same domain.
Advantages:    breaks very few or no sites
Disadvantages: confusing to web page authors and creators of other browsers.

3. Leave history.length as UniversalBrowserRead, and try to convince travelocity
to change their site.
Advantages:    sets a simple precedent that newer browsers can follow, says that
               Mozilla is committed to privacy.
Disadvantages: breaks a few sites.
Keywords: nsBranchvbranch
Whiteboard: [pdt+]
I appreciate that Security comes at this from a different, and valued, 
perspective, but I still don't see any compelling privacy issue here. In 
fact, I think that I had the right fix originally as 'allAccess', since 
that is what it has ~always been (or at least in Nav2.x and Nav4.x, haven't 
checked Nav3.x).

This has been like this since Javascript first came out! And now, because
this was unintentionally closed off for a while, this becomes something that
needs "fixing"?

At the end of the day, this is a cage match for jst and mstolz, but I cringe 
at the suggestion that we just cavalierly break any sites without really 
compelling reasons.
I need to correct some misconceptions here. History.length was intentionally
restricted, because of the potential privacy violations Jesse mentions above.
These are not inconsequential, but must be weighed against the inconvenience of
breaking several sites that use this property. Compatibility with major websites
is a compelling argument, "We've always done it this way" is not. If we have to
do things the same way they were done in version 2, then why continue to release
new versions? I'm OK with removing that line in all.js, causing history.length
to revert to the default sameOrigin policy. As Jesse says, this will give us
parity with IE. Let's not make it allAccess only because Netscape has always
done it that way. That's not progress.
*** Bug 91283 has been marked as a duplicate of this bug. ***
Target is now 0.9.4, Priority P1.
Priority: P4 → P1
Target Milestone: mozilla0.9.3 → mozilla0.9.4
*** Bug 92353 has been marked as a duplicate of this bug. ***
Attached patch PatchSplinter Review
Whiteboard: patch
sr=jst
Fix checked in.
Status: ASSIGNED → RESOLVED
Closed: 23 years ago
Resolution: --- → FIXED
Actually, Mitch, I think you missed this with your checkin yesterday. 
Status: RESOLVED → REOPENED
Resolution: FIXED → ---
a=asa on behalf of drivers.
Fix checked in (for real this time).
Status: REOPENED → RESOLVED
Closed: 23 years ago23 years ago
Resolution: --- → FIXED
Verified on 2001-09-19-03 build on WinNT

Wrote the following test case to test the history.length.

<html>
<head>
<script>
function openHistoryWindow () {
	// Open a new window
	var w = window.open ("", "historyWindow", "width=500,height=300,menubar,resizable");
	var d = w.document;

	//Request a privilege
	netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");

	// Output the browsing history of this window as links in the new window
	for (var i = 0; i < history.length; i++) {
		d.write('<A TARGET = "new" HREF="' + history[i] + '">');
		d.write(history[i]);
		d.writeln('</A>');
	}
	d.close ();

	//return the new window
	return w;
}
</script>
</head>
<body>
<form>
<input type="button" onclick="openHistoryWindow();"
           value="Click to get the History">
</form>
</body>
</html>

The test works as expected
Status: RESOLVED → VERIFIED
changed the QA contact to bsharma@netscape.com
QA Contact: ckritzer → bsharma
slight nit. That testcase requests priveleges. The point of this bug was 
that (for better or worse) 'history.length' was, by default, readable. 
This is the minimal testcase:

<p>is there a number (even 0) below? or was an exception thrown</p>
<script>
  var foo = history.length;
  document.write(foo);
</script>

Verified above test case, and got '6' on the browser window.
So, history.length is accessible without privileges which is ecpected.
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: