From 1cd88ff25245d7ec094b9faa58a6eda8aa9a38aa Mon Sep 17 00:00:00 2001 From: Krishna Porwal Date: Wed, 15 Oct 2025 19:51:54 +0530 Subject: [PATCH 1/3] fix(amazon): robust decode of p.state (url/base64/html-entity/unescape) and add cross-platform Node build; update build script and rebuild dist --- dist/hello.all.js | 125 ++++++++++++++++++++++++++++++++---------- dist/hello.all.min.js | 3 +- dist/hello.js | 125 ++++++++++++++++++++++++++++++++---------- dist/hello.min.js | 3 +- package-lock.json | 4 +- package.json | 2 +- scripts/build.js | 97 ++++++++++++++++++++++++++++++++ src/hello.js | 124 ++++++++++++++++++++++++++++++++--------- 8 files changed, 393 insertions(+), 90 deletions(-) create mode 100644 scripts/build.js diff --git a/dist/hello.all.js b/dist/hello.all.js index ba2d384..c46cc27 100644 --- a/dist/hello.all.js +++ b/dist/hello.all.js @@ -1,4 +1,3 @@ -/*! hellojs v1.20.0 - (c) 2012-2023 Andrew Dodson - MIT https://adodson.com/hello.js/LICENSE */ // ES5 Object.create if (!Object.create) { @@ -1453,32 +1452,109 @@ hello.utils.extend(hello.utils, { var p; var location = window.location; - // Is this an auth relay message which needs to call the proxy? - p = _this.param(location.search); + // Helper to robustly decode a state string that may be JSON, URL-encoded, + // HTML-entity encoded, or base64 encoded (provider specific quirks) + function decodeStateString(str) { + if (!str || typeof str !== 'string') { + return null; + } - // OAuth2 or OAuth1 server response? - if (p && p.state && (p.code || p.oauth_token)) { + function tryParse(jsonString) { + try { return JSON.parse(jsonString); } catch (e) { return null; } + } - try { - var state = JSON.parse(p.state); + function decodeURIComponentSafe(s) { + try { return decodeURIComponent(s); } catch (e) { return s; } + } + + function unescapeSafe(s) { + try { return unescape(s); } catch (e) { return s; } + } - // Add this path as the redirect_uri - p.redirect_uri = state.redirect_uri || location.href.replace(/[\?\#].*$/, ''); + function htmlEntityDecode(s) { + // Handle common encodings seen in wild redirects + return s + .replace(/"/g, '"') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/'/g, "'"); + } + + function atobSafe(s) { + try { return window.atob(s); } catch (e) { return null; } + } - // Redirect to the host - var path = _this.qs(state.oauth_proxy, p); + // Try raw JSON first + var obj = tryParse(str); + if (obj) { return obj; } + // Try URL-decoded once + var urlOnce = decodeURIComponentSafe(str); + obj = tryParse(urlOnce); + if (obj) { return obj; } - if (isValidUrl(path)) { - location.assign(path); + // Try unescape (covers some ISO-8859-1 escapes as seen with Amazon) + var unesc = unescapeSafe(str); + obj = tryParse(unesc); + if (obj) { return obj; } + + // Try HTML-entity replacements + var html = htmlEntityDecode(str); + obj = tryParse(html); + if (obj) { return obj; } + + // Try base64 (some providers require/return base64 state) + // Attempt straight, url-decoded, and HTML-entity cleaned variants + var candidates = [str, urlOnce, html]; + for (var i = 0; i < candidates.length; i++) { + var c = candidates[i]; + // Heuristic: base64 charset and padding + if (/^[A-Za-z0-9+/=_-]+$/.test(c)) { + // Replace URL-safe base64 chars if present + var normalized = c.replace(/-/g, '+').replace(/_/g, '/'); + var padded = normalized + Array((4 - (normalized.length % 4)) % 4 + 1).join('='); + var b64 = atobSafe(padded); + if (b64) { + obj = tryParse(b64); + if (obj) { return obj; } + } } + } - return; + // Last attempt: decodeURIComponent + unescape combo (legacy kludge) + var combo; + try { combo = decodeURIComponent(unescape(str)); } catch (e) { combo = null; } + if (combo) { + obj = tryParse(combo); + if (obj) { return obj; } } - catch (e) { - console.error('Could not decode state parameter', e); + + return null; + } + + // Is this an auth relay message which needs to call the proxy? + p = _this.param(location.search); + + // OAuth2 or OAuth1 server response? + if (p && p.state && (p.code || p.oauth_token)) { + + var state = decodeStateString(p.state); + if (!state) { + console.error('Could not decode state parameter'); return; } + + // Add this path as the redirect_uri + p.redirect_uri = state.redirect_uri || location.href.replace(/[\?\#].*$/, ''); + + // Redirect to the host + var path = _this.qs(state.oauth_proxy, p); + + if (isValidUrl(path)) { + location.assign(path); + } + + return; } // Save session, from redirected authentication @@ -1494,19 +1570,12 @@ hello.utils.extend(hello.utils, { // Remove any addition information // E.g. p.state = 'facebook.page'; - try { - var a = JSON.parse(p.state); - _this.extend(p, a); + var parsedState = decodeStateString(p.state); + if (parsedState) { + _this.extend(p, parsedState); } - catch (e) { - var stateDecoded = decodeURIComponent(p.state); - try { - var b = JSON.parse(stateDecoded); - _this.extend(p, b); - } - catch (e) { - console.error('Could not decode state parameter'); - } + else { + console.error('Could not decode state parameter'); } // Access_token? diff --git a/dist/hello.all.min.js b/dist/hello.all.min.js index f8b6665..1f6a0a7 100644 --- a/dist/hello.all.min.js +++ b/dist/hello.all.min.js @@ -1,2 +1 @@ -/*! hellojs v1.20.0 - (c) 2012-2023 Andrew Dodson - MIT https://adodson.com/hello.js/LICENSE */ -if(!Object.create){Object.create=function(){function F(){}return function(o){if(arguments.length!=1){throw new Error("Object.create implementation only accepts one parameter.")}F.prototype=o;return new F}}()}if(!Object.keys){Object.keys=function(o,k,r){r=[];for(k in o){if(r.hasOwnProperty.call(o,k))r.push(k)}return r}}if(!Array.prototype.indexOf){Array.prototype.indexOf=function(s){for(var j=0;j>>0;if(typeof fun!=="function"){throw new TypeError}var thisArg=arguments.length>=2?arguments[1]:void 0;for(var i=0;i(new Date).getTime()/1e3){var diff=utils.diff((session.scope||"").split(SCOPE_SPLIT),(p.qs.state.scope||"").split(SCOPE_SPLIT));if(diff.length===0){promise.fulfill({unchanged:true,network:p.network,authResponse:session});return promise}}}if(opts.display==="page"&&opts.page_uri){p.qs.state.page_uri=utils.url(opts.page_uri).href}if("login"in provider&&typeof provider.login==="function"){provider.login(p)}if(!/\btoken\b/.test(responseType)||parseInt(provider.oauth.version,10)<2||opts.display==="none"&&provider.oauth.grant&&session&&session.refresh_token){p.qs.state.oauth=provider.oauth;p.qs.state.oauth_proxy=opts.oauth_proxy}if(provider.oauth.base64_state){p.qs.state=window.btoa(JSON.stringify(p.qs.state))}else{p.qs.state=encodeURIComponent(JSON.stringify(p.qs.state))}if(parseInt(provider.oauth.version,10)===1){url=utils.qs(opts.oauth_proxy,p.qs,encodeFunction)}else if(opts.display==="none"&&provider.oauth.grant&&session&&session.refresh_token){p.qs.refresh_token=session.refresh_token;url=utils.qs(opts.oauth_proxy,p.qs,encodeFunction)}else{url=utils.qs(provider.oauth.auth,p.qs,encodeFunction)}emit("auth.init",p);if(opts.display==="none"){utils.iframe(url,redirectUri)}else if(opts.display==="popup"){var popup=utils.popup(url,redirectUri,opts.popup);var timer=setInterval(function(){if(!popup||popup.closed){clearInterval(timer);if(!promise.state){var response=error("cancelled","Login has been cancelled");if(!popup){response=error("blocked","Popup was blocked")}response.network=p.network;promise.reject(response)}}},100)}else{window.location=url}return promise.proxy;function encodeFunction(s){return s}function filterEmpty(s){return!!s}},logout:function(){var _this=this;var utils=_this.utils;var error=utils.error;var promise=utils.Promise();var p=utils.args({name:"s",options:"o",callback:"f"},arguments);p.options=p.options||{};promise.proxy.then(p.callback,p.callback);function emit(s,value){hello.emit(s,value)}promise.proxy.then(emit.bind(this,"auth.logout auth"),emit.bind(this,"error"));p.name=p.name||this.settings.default_service;p.authResponse=utils.store(p.name);if(p.name&&!(p.name in _this.services)){promise.reject(error("invalid_network","The network was unrecognized"))}else if(p.name&&p.authResponse){var callback=function(opts){utils.store(p.name,null);promise.fulfill(hello.utils.merge({network:p.name},opts||{}))};var _opts={};if(p.options.force){var logout=_this.services[p.name].logout;if(logout){if(typeof logout==="function"){logout=logout(callback,p)}if(typeof logout==="string"){utils.iframe(logout);_opts.force=null;_opts.message="Logout success on providers site was indeterminate"}else if(logout===undefined){return promise.proxy}}}callback(_opts)}else{promise.reject(error("invalid_session","There was no session to remove"))}return promise.proxy},getAuthResponse:function(service){service=service||this.settings.default_service;if(!service||!(service in this.services)){return null}return this.utils.store(service)||null},events:{}});hello.utils.extend(hello.utils,{error:function(code,message){return{error:{code:code,message:message}}},qs:function(url,params,formatFunction){if(params){formatFunction=formatFunction||encodeURIComponent;for(var x in params){var str="([\\?\\&])"+x+"=[^\\&]*";var reg=new RegExp(str);if(url.match(reg)){url=url.replace(reg,"$1"+x+"="+formatFunction(params[x]));delete params[x]}}}if(!this.isEmpty(params)){return url+(url.indexOf("?")>-1?"&":"?")+this.param(params,formatFunction)}return url},param:function(s,formatFunction){var b;var a={};var m;if(typeof s==="string"){formatFunction=formatFunction||decodeURIComponent;m=s.replace(/^[\#\?]/,"").match(/([^=\/\&]+)=([^\&]+)/g);if(m){for(var i=0;i-1&&t==="string"||o[x].indexOf("o")>-1&&t==="object"||o[x].indexOf("i")>-1&&t==="number"||o[x].indexOf("a")>-1&&t==="object"||o[x].indexOf("f")>-1&&t==="function")){p[x]=args[i++]}else if(typeof o[x]==="string"&&o[x].indexOf("!")>-1){return false}}}return p},url:function(path){if(!path){return window.location}else if(window.URL&&URL instanceof Function&&URL.length!==0){return new URL(path,window.location)}else{var a=document.createElement("a");a.href=path;return a.cloneNode(false)}},diff:function(a,b){return b.filter(function(item){return a.indexOf(item)===-1})},diffKey:function(a,b){if(a||!b){var r={};for(var x in a){if(!(x in b)){r[x]=a[x]}}return r}return a},unique:function(a){if(!Array.isArray(a)){return[]}return a.filter(function(item,index){return a.indexOf(item)===index})},isEmpty:function(obj){if(!obj)return true;if(Array.isArray(obj)){return!obj.length}else if(typeof obj==="object"){for(var key in obj){if(obj.hasOwnProperty(key)){return false}}}return true},Promise:function(){var STATE_PENDING=0;var STATE_FULFILLED=1;var STATE_REJECTED=2;var api=function(executor){if(!(this instanceof api))return new api(executor);this.id="Thenable/1.0.6";this.state=STATE_PENDING;this.fulfillValue=undefined;this.rejectReason=undefined;this.onFulfilled=[];this.onRejected=[];this.proxy={then:this.then.bind(this)};if(typeof executor==="function")executor.call(this,this.fulfill.bind(this),this.reject.bind(this))};api.prototype={fulfill:function(value){return deliver(this,STATE_FULFILLED,"fulfillValue",value)},reject:function(value){return deliver(this,STATE_REJECTED,"rejectReason",value)},then:function(onFulfilled,onRejected){var curr=this;var next=new api;curr.onFulfilled.push(resolver(onFulfilled,next,"fulfill"));curr.onRejected.push(resolver(onRejected,next,"reject"));execute(curr);return next.proxy}};var deliver=function(curr,state,name,value){if(curr.state===STATE_PENDING){curr.state=state;curr[name]=value;execute(curr)}return curr};var execute=function(curr){if(curr.state===STATE_FULFILLED)execute_handlers(curr,"onFulfilled",curr.fulfillValue);else if(curr.state===STATE_REJECTED)execute_handlers(curr,"onRejected",curr.rejectReason)};var execute_handlers=function(curr,name,value){if(curr[name].length===0)return;var handlers=curr[name];curr[name]=[];var func=function(){for(var i=0;i-1){for(var i=0;i=400:typeof r==="object"&&"error"in r){promise.reject(r)}else{promise.fulfill(r)}return}if(r===true){r={success:true}}else if(!r){r={}}if(p.method==="delete"){r=!r||utils.isEmpty(r)?{success:true}:r}if(o.wrap&&(p.path in o.wrap||"default"in o.wrap)){var wrap=p.path in o.wrap?p.path:"default";var time=(new Date).getTime();var b=o.wrap[wrap](r,headers,p);if(b){r=b}}if(r&&"paging"in r&&r.paging.next){if(r.paging.next[0]==="?"){r.paging.next=p.path+r.paging.next}else{r.paging.next+="#"+p.path}}if(!r||"error"in r){promise.reject(r)}else{promise.fulfill(r)}})}};hello.utils.extend(hello.utils,{request:function(p,callback){var _this=this;var error=_this.error;if(!_this.isEmpty(p.data)&&!("FileList"in window)&&_this.hasBinary(p.data)){p.xhr=false;p.jsonp=false}var cors=this.request_cors(function(){return p.xhr===undefined||p.xhr&&(typeof p.xhr!=="function"||p.xhr(p,p.query))});if(cors){formatUrl(p,function(url){var x=_this.xhr(p.method,url,p.headers,p.data,callback);x.onprogress=p.onprogress||null;if(x.upload&&p.onuploadprogress){x.upload.onprogress=p.onuploadprogress}});return}var _query=p.query;p.query=_this.clone(p.query);p.callbackID=_this.globalEvent();if(p.jsonp!==false){p.query.callback=p.callbackID;if(typeof p.jsonp==="function"){p.jsonp(p,p.query)}if(p.method==="get"){formatUrl(p,function(url){_this.jsonp(url,callback,p.callbackID,p.timeout)});return}else{p.query=_query}}if(p.form!==false){p.query.redirect_uri=p.redirect_uri;p.query.state=JSON.stringify({callback:p.callbackID});var opts;if(typeof p.form==="function"){opts=p.form(p,p.query)}if(p.method==="post"&&opts!==false){formatUrl(p,function(url){_this.post(url,p.data,opts,callback,p.callbackID,p.timeout)});return}}callback(error("invalid_request","There was no mechanism for handling this request"));return;function formatUrl(p,callback){var sign;if(p.authResponse&&p.authResponse.oauth&&parseInt(p.authResponse.oauth.version,10)===1){sign=p.query.access_token;delete p.query.access_token;p.proxy=true}if(p.data&&(p.method==="get"||p.method==="delete")){_this.extend(p.query,p.data);p.data=null}var path=_this.qs(p.url,p.query);if(p.proxy){path=_this.qs(p.oauth_proxy,{path:path,access_token:sign||"",then:p.proxy_response_type||(p.method.toLowerCase()==="get"?"redirect":"proxy"),method:p.method.toLowerCase(),suppress_response_codes:p.suppress_response_codes||true})}callback(path)}},request_cors:function(callback){return"withCredentials"in new XMLHttpRequest&&callback()},domInstance:function(type,data){var test="HTML"+(type||"").replace(/^[a-z]/,function(m){return m.toUpperCase()})+"Element";if(!data){return false}if(window[test]){return data instanceof window[test]}else if(window.Element){return data instanceof window.Element&&(!type||data.tagName&&data.tagName.toLowerCase()===type)}else{return!(data instanceof Object||data instanceof Array||data instanceof String||data instanceof Number)&&data.tagName&&data.tagName.toLowerCase()===type}},clone:function(obj){if(obj===null||typeof obj!=="object"||obj instanceof Date||"nodeName"in obj||this.isBinary(obj)||typeof FormData==="function"&&obj instanceof FormData){return obj}if(Array.isArray(obj)){return obj.map(this.clone.bind(this))}var clone={};for(var x in obj){clone[x]=this.clone(obj[x])}return clone},xhr:function(method,url,headers,data,callback){var r=new XMLHttpRequest;var error=this.error;var binary=false;if(method==="blob"){binary=method;method="GET"}method=method.toUpperCase();r.onload=function(e){var json=r.response;try{json=JSON.parse(r.responseText)}catch(_e){if(r.status===401){json=error("access_denied",r.statusText)}}var headers=headersToJSON(r.getAllResponseHeaders());headers.statusCode=r.status;callback(json||(method==="GET"?error("empty_response","Could not get resource"):{}),headers)};r.onerror=function(e){var json=r.responseText;try{json=JSON.parse(r.responseText)}catch(_e){}callback(json||error("access_denied","Could not get resource"))};var x;if(method==="GET"||method==="DELETE"){data=null}else if(data&&typeof data!=="string"&&!(data instanceof FormData)&&!(data instanceof File)&&!(data instanceof Blob)){var f=new FormData;for(x in data)if(data.hasOwnProperty(x)){if(data[x]instanceof HTMLInputElement){if("files"in data[x]&&data[x].files.length>0){f.append(x,data[x].files[0])}}else if(data[x]instanceof Blob){f.append(x,data[x],data.name)}else{f.append(x,data[x])}}data=f}r.open(method,url,true);if(binary){if("responseType"in r){r.responseType=binary}else{r.overrideMimeType("text/plain; charset=x-user-defined")}}if(headers){for(x in headers){r.setRequestHeader(x,headers[x])}}r.send(data);return r;function headersToJSON(s){var r={};var reg=/([a-z\-]+):\s?(.*);?/gi;var m;while(m=reg.exec(s)){r[m[1]]=m[2]}return r}},jsonp:function(url,callback,callbackID,timeout){var _this=this;var error=_this.error;var bool=0;var head=document.getElementsByTagName("head")[0];var operaFix;var result=error("server_error","server_error");var cb=function(){if(!bool++){window.setTimeout(function(){callback(result);head.removeChild(script)},0)}};callbackID=_this.globalEvent(function(json){result=json;return true},callbackID);url=url.replace(new RegExp("=\\?(&|$)"),"="+callbackID+"$1");var script=_this.append("script",{id:callbackID,name:callbackID,src:url,async:true,onload:cb,onerror:cb,onreadystatechange:function(){if(/loaded|complete/i.test(this.readyState)){cb()}}});if(window.navigator.userAgent.toLowerCase().indexOf("opera")>-1){operaFix=_this.append("script",{text:"document.getElementById('"+callbackID+"').onerror();"});script.async=false}if(timeout){window.setTimeout(function(){result=error("timeout","timeout");cb()},timeout)}head.appendChild(script);if(operaFix){head.appendChild(operaFix)}},post:function(url,data,options,callback,callbackID,timeout){var _this=this;var error=_this.error;var doc=document;var form=null;var reenableAfterSubmit=[];var newform;var i=0;var x=null;var bool=0;var cb=function(r){if(!bool++){callback(r)}};_this.globalEvent(cb,callbackID);var win;try{win=doc.createElement('