Skip to content

Commit d224373

Browse files
wti806bojeil-google
authored andcommitted
- Sanitizes all URLs passed via query parameters to prevent JS inject… (#345)
* - Sanitizes all URLs passed via query parameters to prevent JS injection via URL. - Appends photo size for profile image hosted in Google in demo app. PiperOrigin-RevId: 188789358 Change-Id: Ib3faa82cdb8efcd9b91edd66a0fc1df28d14d9d4 * updated readme and change log
1 parent 9bb9191 commit d224373

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+3910
-432
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,17 @@ Localized versions of the widget are available through the CDN. To use a localiz
6363
localized JS library instead of the default library:
6464

6565
```html
66-
<script src="https://www.gstatic.com/firebasejs/ui/2.6.2/firebase-ui-auth__{LANGUAGE_CODE}.js"></script>
67-
<link type="text/css" rel="stylesheet" href="https://www.gstatic.com/firebasejs/ui/2.6.2/firebase-ui-auth.css" />
66+
<script src="https://www.gstatic.com/firebasejs/ui/2.6.3/firebase-ui-auth__{LANGUAGE_CODE}.js"></script>
67+
<link type="text/css" rel="stylesheet" href="https://www.gstatic.com/firebasejs/ui/2.6.3/firebase-ui-auth.css" />
6868
```
6969

7070
where `{LANGUAGE_CODE}` is replaced by the code of the language you want. For example, the French
7171
version of the library is available at
72-
`https://www.gstatic.com/firebasejs/ui/2.6.2/firebase-ui-auth__fr.js`. The list of available
72+
`https://www.gstatic.com/firebasejs/ui/2.6.3/firebase-ui-auth__fr.js`. The list of available
7373
languages and their respective language codes can be found at [LANGUAGES.md](LANGUAGES.md).
7474

7575
Right-to-left languages also require the right-to-left version of the stylesheet, available at
76-
`https://www.gstatic.com/firebasejs/ui/2.6.2/firebase-ui-auth-rtl.css`, instead of the default
76+
`https://www.gstatic.com/firebasejs/ui/2.6.3/firebase-ui-auth-rtl.css`, instead of the default
7777
stylesheet. The supported right-to-left languages are Arabic (ar), Farsi (fa), and Hebrew (iw).
7878

7979
### Option 2: npm Module

changelog.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
fixed - Sanitizes all URLs passed via query parameters to prevent JS injection via URL.
2+
fixed - Appends photo size for profile image hosted in Google in demo app.

demo/public/app.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,16 @@ var handleSignedInUser = function(user) {
112112
document.getElementById('email').textContent = user.email;
113113
document.getElementById('phone').textContent = user.phoneNumber;
114114
if (user.photoURL){
115-
document.getElementById('photo').src = user.photoURL;
115+
var photoURL = user.photoURL;
116+
// Append size to the photo URL for Google hosted images to avoid requesting
117+
// the image with its original resolution (using more bandwidth than needed)
118+
// when it is going to be presented in smaller size.
119+
if ((photoURL.indexOf('googleusercontent.com') != -1) ||
120+
(photoURL.indexOf('ggpht.com') != -1)) {
121+
photoURL = photoURL + '?sz=' +
122+
document.getElementById('photo').clientHeight;
123+
}
124+
document.getElementById('photo').src = photoURL;
116125
document.getElementById('photo').style.display = 'block';
117126
} else {
118127
document.getElementById('photo').style.display = 'none';

javascript/utils/util.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ goog.require('goog.Promise');
2222
goog.require('goog.dom');
2323
goog.require('goog.events');
2424
goog.require('goog.events.EventType');
25+
goog.require('goog.html.SafeUrl');
2526
goog.require('goog.userAgent');
2627
goog.require('goog.window');
2728

@@ -32,9 +33,21 @@ goog.require('goog.window');
3233
* some browsers don't allow overwriting of the native object.
3334
*
3435
* @param {string} url The target URL.
36+
* @param {?Window=} opt_win Optional window whose location is to be reassigned.
3537
*/
36-
firebaseui.auth.util.goTo = function(url) {
37-
window.location.assign(url);
38+
firebaseui.auth.util.goTo = function(url, opt_win) {
39+
var win = opt_win || window;
40+
// Sanitize URL before redirect.
41+
win.location.assign(firebaseui.auth.util.sanitizeUrl(url));
42+
};
43+
44+
45+
/**
46+
* @param {string} url The plain unsafe URL.
47+
* @return {string} The sanitized safe version of the provided URL.
48+
*/
49+
firebaseui.auth.util.sanitizeUrl = function(url) {
50+
return goog.html.SafeUrl.unwrap(goog.html.SafeUrl.sanitize(url));
3851
};
3952

4053

@@ -67,9 +80,13 @@ firebaseui.auth.util.goBack = function() {
6780
* since some browsers don't allow to overwrite the native object.
6881
*
6982
* @param {string} url The target URL.
83+
* @param {?Window=} opt_win Optional window whose parent's location is to
84+
* be reassigned.
7085
*/
71-
firebaseui.auth.util.openerGoTo = function(url) {
72-
window.opener.location.assign(url);
86+
firebaseui.auth.util.openerGoTo = function(url, opt_win) {
87+
var win = opt_win || window;
88+
// Sanitize URL before redirect.
89+
win.opener.location.assign(firebaseui.auth.util.sanitizeUrl(url));
7390
};
7491

7592

javascript/utils/util_test.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,95 @@ function testOnDomReady() {
117117
// Should resolve immediately without any error.
118118
return firebaseui.auth.util.onDomReady();
119119
}
120+
121+
122+
function testGoTo_safeUrl() {
123+
var mockWin = {
124+
location: {
125+
assign: goog.testing.recordFunction()
126+
}
127+
};
128+
stubs.reset();
129+
130+
// Confirm goTo redirects as expected to safe URLs.
131+
firebaseui.auth.util.goTo(
132+
'https://www.example.com:80/path/?a=1#b=2', mockWin);
133+
assertEquals(1, mockWin.location.assign.getCallCount());
134+
assertEquals(
135+
'https://www.example.com:80/path/?a=1#b=2',
136+
mockWin.location.assign.getLastCall().getArgument(0));
137+
}
138+
139+
140+
function testGoTo_unsafeUrl() {
141+
var mockWin = {
142+
location: {
143+
assign: goog.testing.recordFunction()
144+
}
145+
};
146+
stubs.reset();
147+
148+
// Confirm URLs are sanitized before redirection.
149+
firebaseui.auth.util.goTo(
150+
'javascript:doEvilStuff()', mockWin);
151+
assertEquals(1, mockWin.location.assign.getCallCount());
152+
assertEquals(
153+
'about:invalid#zClosurez',
154+
mockWin.location.assign.getLastCall().getArgument(0));
155+
}
156+
157+
158+
function testOpenerGoTo_safeUrl() {
159+
var mockWin = {
160+
opener: {
161+
location: {
162+
assign: goog.testing.recordFunction()
163+
}
164+
}
165+
};
166+
stubs.reset();
167+
168+
// Confirm openerGoTo redirects as expected to safe URLs.
169+
firebaseui.auth.util.openerGoTo(
170+
'https://www.example.com:80/path/?a=1#b=2', mockWin);
171+
assertEquals(1, mockWin.opener.location.assign.getCallCount());
172+
assertEquals(
173+
'https://www.example.com:80/path/?a=1#b=2',
174+
mockWin.opener.location.assign.getLastCall().getArgument(0));
175+
}
176+
177+
178+
function testOpenerGoTo_unsafeUrl() {
179+
var mockWin = {
180+
opener: {
181+
location: {
182+
assign: goog.testing.recordFunction()
183+
}
184+
}
185+
};
186+
stubs.reset();
187+
188+
// Confirm URLs are sanitized before redirection.
189+
firebaseui.auth.util.openerGoTo(
190+
'javascript:doEvilStuff()', mockWin);
191+
assertEquals(1, mockWin.opener.location.assign.getCallCount());
192+
assertEquals(
193+
'about:invalid#zClosurez',
194+
mockWin.opener.location.assign.getLastCall().getArgument(0));
195+
}
196+
197+
198+
function testSanitizeUrl_safeUrl() {
199+
assertEquals(
200+
'https://www.example.com:80/path/?a=1#b=2',
201+
firebaseui.auth.util.sanitizeUrl(
202+
'https://www.example.com:80/path/?a=1#b=2'));
203+
}
204+
205+
206+
function testSanitizeUrl_unsafeUrl() {
207+
assertEquals(
208+
'about:invalid#zClosurez',
209+
firebaseui.auth.util.sanitizeUrl('javascript:doEvilStuff()'));
210+
}
211+

javascript/widgets/authui.js

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -911,7 +911,7 @@ firebaseui.auth.AuthUI.prototype.startSignInWithEmailAndPassword =
911911
* Create a new email and password account.
912912
* @param {string} email The email to sign up with.
913913
* @param {string} password The password to sign up with.
914-
* @return {!firebase.Promise<!firebase.User>}
914+
* @return {!firebase.Promise<!firebase.auth.UserCredential>}
915915
*/
916916
firebaseui.auth.AuthUI.prototype.startCreateUserWithEmailAndPassword =
917917
function(email, password) {
@@ -925,17 +925,15 @@ firebaseui.auth.AuthUI.prototype.startCreateUserWithEmailAndPassword =
925925
// For anonymous user upgrade, call link with credential on the external
926926
// Auth user. Otherwise merge conflict will always occur when linking the
927927
// credential to the external anonymous user after creation.
928-
return /** @type {!firebase.Promise<!firebase.User>} */ (
929-
user.linkAndRetrieveDataWithCredential(credential)
930-
.then(function(result) {
931-
return result['user'];
932-
}));
928+
return /** @type {!firebase.Promise<!firebase.auth.UserCredential>} */ (
929+
user.linkAndRetrieveDataWithCredential(credential));
933930
} else {
934931
// Start create user with email and password. This runs on the internal
935932
// Auth instance as finish sign in will sign in with that same credential
936933
// to developer Auth instance.
937-
return /** @type {!firebase.Promise<!firebase.User>} */ (
938-
self.getAuth().createUserWithEmailAndPassword(email, password));
934+
return /** @type {!firebase.Promise<!firebase.auth.UserCredential>} */ (
935+
self.getAuth().createUserAndRetrieveDataWithEmailAndPassword(
936+
email, password));
939937
}
940938
};
941939
// Initialize current user if auto upgrade is enabled beforing running
@@ -1149,24 +1147,28 @@ firebaseui.auth.AuthUI.prototype.finishSignInWithCredential =
11491147
this.checkIfDestroyed_();
11501148
var self = this;
11511149
var cb = function(user) {
1152-
// Anonymous user upgrade successful, resolve immediately with the user.
1153-
// No need to sign in again with the same credential on the external Auth
1154-
// instance.
1150+
// Anonymous user upgrade successful, sign out on internal instance and
1151+
// resolve with the user. No need to sign in again with the same credential
1152+
// on the external Auth instance.
11551153
// If user is signed in on internal instance, ignore the user on external
1156-
// instance and finish the sign in on external instance.
1154+
// instance, sign out on internal instance and finish the sign in on
1155+
// external instance.
11571156
if (self.currentUser_ &&
11581157
!self.currentUser_['isAnonymous'] &&
11591158
self.getConfig().autoUpgradeAnonymousUsers() &&
11601159
!self.getAuth().currentUser) {
1161-
return (firebase.Promise || goog.Promise).resolve(self.currentUser_);
1160+
return self.clearTempAuthState().then(function() {
1161+
return self.currentUser_;
1162+
});
11621163
} else if (user) {
11631164
// TODO: optimize and fail directly as this will fail in most cases
11641165
// with error credential already in use.
11651166
// There are cases where this is required. For example, when email
11661167
// mismatch occurs and the user continues with the new account.
11671168
return /** @type {!firebase.Promise<!firebase.User>} */ (
1168-
user.linkWithCredential(credential)
1169-
.then(function() {
1169+
self.clearTempAuthState().then(function() {
1170+
return user.linkWithCredential(credential);
1171+
}).then(function() {
11701172
return user;
11711173
}, function(error) {
11721174
// Rethrow email already in use error so it can trigger the account
@@ -1183,7 +1185,84 @@ firebaseui.auth.AuthUI.prototype.finishSignInWithCredential =
11831185
// Finishes sign in with the supplied credential on the developer provided
11841186
// Auth instance. On completion, this will redirect to signInSuccessUrl or
11851187
// trigger the signInSuccess callback.
1186-
return self.getExternalAuth().signInWithCredential(credential);
1188+
return self.clearTempAuthState().then(function() {
1189+
return self.getExternalAuth().signInWithCredential(credential);
1190+
});
1191+
}
1192+
};
1193+
// Initialize current user if auto upgrade is enabled beforing running
1194+
// callback and returning result.
1195+
return this.initializeForAutoUpgrade_(cb);
1196+
};
1197+
1198+
1199+
/**
1200+
* Finishes FirebaseUI login with the given 3rd party credentials.
1201+
* @param {!firebaseui.auth.AuthResult} authResult The Auth result.
1202+
* @return {!firebase.Promise<!firebaseui.auth.AuthResult>}
1203+
*/
1204+
firebaseui.auth.AuthUI.prototype.finishSignInAndRetrieveDataWithAuthResult =
1205+
function(authResult) {
1206+
// Check if instance is already destroyed.
1207+
this.checkIfDestroyed_();
1208+
var self = this;
1209+
var cb = function(user) {
1210+
if (self.currentUser_ &&
1211+
!self.currentUser_['isAnonymous'] &&
1212+
self.getConfig().autoUpgradeAnonymousUsers() &&
1213+
!self.getAuth().currentUser) {
1214+
return self.clearTempAuthState().then(function() {
1215+
// Do not expose password Auth credential to signInSuccess callback.
1216+
if (authResult['credential']['providerId'] == 'password') {
1217+
authResult['credential'] = null;
1218+
}
1219+
return authResult;
1220+
});
1221+
} else if (user) {
1222+
// TODO: optimize and fail directly as this will fail in most cases
1223+
// with error credential already in use.
1224+
// There are cases where this is required. For example, when email
1225+
// mismatch occurs and the user continues with the new account.
1226+
return /** @type {!firebase.Promise<!firebaseui.auth.AuthResult>} */ (
1227+
self.clearTempAuthState().then(function() {
1228+
return user.linkAndRetrieveDataWithCredential(
1229+
authResult['credential']);
1230+
}).then(function(userCredential) {
1231+
authResult['user'] = userCredential['user'];
1232+
authResult['credential'] = userCredential['credential'];
1233+
authResult['operationType'] = userCredential['operationType'];
1234+
authResult['additionalUserInfo'] =
1235+
userCredential['additionalUserInfo'];
1236+
return authResult;
1237+
}, function(error) {
1238+
// Rethrow email already in use error so it can trigger the account
1239+
// linking flow.
1240+
if (error &&
1241+
error['code'] == 'auth/email-already-in-use' &&
1242+
error['email'] && error['credential']) {
1243+
throw error;
1244+
}
1245+
// For all other errors, run onUpgrade check.
1246+
return self.onUpgradeError(error, authResult['credential']);
1247+
}));
1248+
} else {
1249+
// Finishes sign in with the supplied credential on the developer provided
1250+
// Auth instance. On completion, this will redirect to signInSuccessUrl or
1251+
// trigger the signInSuccessWithAuthResult callback.
1252+
if (!authResult['credential']) {
1253+
throw new Error('No credential found!');
1254+
}
1255+
return self.clearTempAuthState().then(function() {
1256+
return self.getExternalAuth().signInAndRetrieveDataWithCredential(
1257+
authResult['credential']);
1258+
}).then(function(userCredential) {
1259+
authResult['user'] = userCredential['user'];
1260+
authResult['credential'] = userCredential['credential'];
1261+
authResult['operationType'] = userCredential['operationType'];
1262+
// AdditionalUserInfo should remain the same as isNewUser field should
1263+
// be the one returned in the first sign in attempt.
1264+
return authResult;
1265+
});
11871266
}
11881267
};
11891268
// Initialize current user if auto upgrade is enabled beforing running

0 commit comments

Comments
 (0)