I know how to handle this in ASP.NET, but is there a way to force the Classic ASP Session ID to be cleared? It is a randomly generated ID like ASPSESSIONIDG32423E that does not seem to be available in RESPONSE.COOKIES collection thus I can not clear it. We have a class ASP site still hanging around and recently it was an audit finding that after the user logs out the same session ID is reused.
MORE CLARIFICATION:
First visit to page, I see this in the proxy editor in Response:
Set-Cookie: ASPSESSIONID=PUYQGHUMEAAJPUYL; path=/Webapp
After a logout, I call Session.RemoveAll and Session.Abandon and then redirect user to login page. At which point I should see a new Set-Cookie with a different value for SessionID. Instead, I do not get a new cookie and the new login session reuses the original session cookie. This is an audit finding that we have to resolve in some way but there does not seem to be a way to control this.
So I did come up with a solution for this as follows. I added two pages called Start.asp and Start2.asp. The original login page was changed to check for a post variable which is now set on Start2.asp, so if login.asp does not see that post variable, it redirects to Start.asp. Start.asp invalidates the ASPSessionID by setting it to 0. The key is using Response.AddHeader "Set-Cookie" in order to do this since Response.Cookies("ASPSESSIONID...") gives an error that you can't access the element:
Code for Start.ASP
<%
If instr(Request.ServerVariables("HTTP_COOKIE"), "ASPSESSIONID") > 0 Then
Dim Allcookies
AllCookies = Split(Request.ServerVariables("HTTP_COOKIE"),";")
For i = 1 to UBound(AllCookies)
If instr(AllCookies(i), "ASPSESSIONID") > 0 Then
Response.AddHeader "Set-Cookie", Left(AllCookies(i),instr(AllCookies(i),"=") -1) & "=0; path=/;secure;httponly"
End if
Next
End if
Response.Redirect("start2.asp")
%>
Next, it calls Start2.asp which looks for all ASPSEssionID cookies and appends Secure; httponly (I had to add these for another finding, ASP metabase setting to add secure only works if the SSL cert. is on the web server. In our case the SSL cert is on a load balancer in front of the web server).
Code for Start2.asp
<%
'CODE for authorization/authentication
'...
Session.Contents.RemoveAll
Session.Abandon
If instr(Request.ServerVariables("HTTP_COOKIE"), "ASPSESSIONID") > 0 Then
Dim Allcookies
AllCookies = Split(Request.ServerVariables("HTTP_COOKIE"),";")
For i = 1 to UBound(AllCookies)
if left(Request.ServerVariables("HTTP_HOST"),2) = "65" and instr(AllCookies(i), "ASPSESSIONID") > 0 Then
Response.AddHeader "Set-Cookie", AllCookies(i) & "; path=/;secure;httponly"
End if
Next
End if
%>
<html>
<body>
<form action="login.asp" method="post">
<input type="hidden" name="start2" id="start2" value="Yes" />
</form>
<script type="text/javascript">
document.forms[0].submit();
</script>
</body>
</html>
Really, though, the new ASPSessionID is not generated until within Start2.asp so that Set-Cookie code for secure and httponly has to also be done in login.asp. So the same code above was copied to the top of login.asp just after this code:
If request.form("Start2") = "" Then
Response.Redirect("start.asp")
End if
IMO - you need to end the session rather than just clear out the session ID. In this case, Session.Abandon is the solution. Ref.: https://devguru.com/content/technologies/asp/session-abandon.html
HTH.
This relates to ASP.NET but describes the behaviour you are seeing in ASP
When you abandon a session, the session ID cookie is not removed from
the browser of the user. Therefore, as soon as the session has been
abandoned, any new requests to the same application will use the same
session ID but will have a new session state instance.
http://support.microsoft.com/?kbid=899918
This behaviour will only occur if using the same browser for the same session, as soon as the browser is closed the session cookie will be lost (providing an explicit expiry date has not been set).
You could try calling Session.Abandon then redirect the user to a page which uses JavaScript to clear all cookies, then redirect to the login page, or whatever page you like.
Clearing all cookies with JavaScript
Related
I am looking to write some code that captures the user's domain name and uses it to either allow them access or not allow them access to the page content.
I am talking about the same thing as restricting access to a page via
IP address but using the domain instead. I.E. yoursite.com ... I am
trying to keep yoursite.com from having access to my form. This is an
effort to restrict access from Russia domain that uses one domain but
multiple ever-changing IP addresses.
It depends what you mean by "having access". If you're wanting to prevent a website from using/spamming a form that you host, then there's a few things you can do. Start by implementing CORS restrictions in your web.config file:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="X-Frame-Options" value="SAMEORIGIN" />
<add name="Access-Control-Allow-Origin" value="https://yourdomain.com" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
Set Access-Control-Allow-Origin to your own domain. This will prevent JavaScript on another website from being able to make POST or GET request to your form.
Set X-Frame-Options to SAMEORIGIN
This will prevent other websites from being able to display your site in an iFrame.
You should also use CSRF tokens. A third party website accessing a form that you host isn't strictly a CSRF issue (actual CSRF exploits tend to use hidden iFrames on the attackers site, which are used to hijack an active session a visitor might have with another website. This is prevented by the other website requiring a token to be included with each form submission). But using CSRF tokens is still good practice and a good deterrent when trying to prevent other websites from having access to a public form that you host.
There are many ways to implement CSRF tokens (and there's lots of tutorials) But this is my preferred method which uses CSRF cookies, as demonstrated in this simple web form:
<%#LANGUAGE="VBSCRIPT" CODEPAGE="65001"%>
<%
response.Charset = "utf-8"
Const CSRF_token_max = 9999999999999
Const CSRF_token_min = 1000000000000
sub create_CSRF_cookie(ignoreFormPost)
' Response.Cookies and Request.Cookies don't behave as you'd expect in Classic ASP.
' Response.Cookies is used to create a new cookie which is return in the HTTP
' response headers, so the client can store it locally.
' Request.Cookies is used to retrieve a cookie from the HTTP request headers, which
' are included in every HTTP request the client makes to the server.
' But here's the catch:
' If you use Response.Cookies to create a new cookie, but during the same page load
' you also use Request.Cookies to retrieve the cookie you have just set, ASP will
' return the value of the cookie waiting to be sent back to the client in the response
' headers, despite the cookie having never left the server.
' This has it's uses, but if we issue a new CSRF cookie before processing a form post,
' Request.Cookies("CSRF") will return the value of the new CSRF cookie rather than the
' value of the cookie contained in the request headers, and thus a CSRF token mismatch
' will occur.
if request.form = "" OR ignoreFormPost then
Dim CSRF_token
Randomize()
CSRF_token = Int((CSRF_token_max-CSRF_token_min+1)*Rnd+CSRF_token_min)
' Save the CSRF token as a cookie.
' HttpOnly must be false, which is it by default in classic asp.
Response.Cookies("CSRF") = CSRF_token
end if
end sub
' Create a new CSRF cookie on each page load
' Set "ignoreFormPost" to false. We don't want to create a new cookie if a form has been posted.
' We will do that once the form has been validated and processed.
call create_CSRF_cookie(false)
'---------------------------------------------------------------
' Process a form POST
'---------------------------------------------------------------
if request.form("formName") = "CSRFform" then
Dim hostName, responseStatus, responseColor, expectedCSRF, postedCSRF, postedText
' Is your domain using http or https?
if Request.ServerVariables("HTTPS") = "on" then _
hostName = "https://" else hostName = "http://"
' Build your hostname, e.g: http://yourdomain.com
hostName = hostName & Request.ServerVariables("HTTP_HOST")
expectedCSRF = request.cookies("CSRF")
postedCSRF = request.Form("CSRF")
postedText = request.Form("text")
' Was the form submitted from your domain?
' Is the CSRF token in the form data the correct length?
' Does the CSRF token in the form data match the CSRF token in the cookie?
if inStr(1,Request.ServerVariables("HTTP_REFERER"),hostName,1) = 1 _
AND len(request.form("CSRF")) = len(CSRF_token_max) _
AND request.Form("CSRF") = request.cookies("CSRF") then
' Everything checks out. Do whatever it is you need to do
responseColor = "#00CD42"
responseStatus = "Form submitted successfully"
else
' verification failed
responseColor = "#E00025"
responseStatus = "Form validation failed"
end if
' Create a new CSRF cookie and tell the sub to ignore the fact that a form was posted.
' We've processed the form now so a new CSRF cookie can be issued.
call create_CSRF_cookie(true)
end if
%><!doctype html>
<html>
<head>
<meta charset="utf-8">
</head>
<%
' We could populate the hidden CSRF field with the "CSRF_token" variable.
' But instead, we're going to retrieve the token from the CSRF cookie
' as the form is submitted and insert it into the CSRF hidden field.
' Why get the CSRF token from the cookie? Well, let's assume you have
' many forms that require a CSRF token. If the user loads a form and
' we populate the CSRF field with the "CSRF_token" variable, great...
' but what if they open another form before submitting this one. A new
' CSRF cookie will be generated. The form will be validated just fine,
' but if they go back to the previous form and submit it, the CSRF cookie
' will have changed and the form will fail to validate.
' This method allows us to generate a new CSRF cookie each time one is
' required, and forms that have already been generated but forgotten
' about will still be verified once the user gets round to submitting it.
' It's quite a common CSRF technique and one I borrowed/stole from Pythons
' Django framework. Ideally, forms should be posted and handled using ajax,
' this would avoid validation fails when resubmitting a form on a page reload
%>
<form method="post" name="CSRFform" onsubmit="getCSRF()">
<table width="<% if request.form("formName") = "CSRFform" then response.write "5" %>0%" border="1" cellspacing="2" cellpadding="6">
<tbody>
<tr>
<td>
<input type="text" name="text" value="<%=Server.HTMLEncode(request.form("text"))%>" placeholder="Enter some text">
<input type="hidden" id="CSRF" name="CSRF" value="">
<input type="hidden" name="formName" value="CSRFform">
<input type="submit" value="Submit">
</td>
</tr>
<% if request.form("formName") = "CSRFform" then %>
<tr>
<td bgcolor="<%=responseColor%>" style="color:#ffffff; font-size:18px; font-weight:bold;"><%=responseStatus%></td>
</tr>
<tr>
<td>
<%
if postedCSRF = "undefined" then postedCSRF = "NA"
if expectedCSRF = "" then expectedCSRF = "NA"
if postedText = "" then postedText = "NA"
response.write "<p><b>Referer</b>: " & Request.ServerVariables("HTTP_REFERER") & "</p>"
response.write "<p><b>Expected Referer</b>: " & hostName & "/*</p><hr>"
response.write "<p><b>CSRF Token</b>: " & postedCSRF & "</p>"
response.write "<p><b>Expected CSRF Token</b>: " & expectedCSRF & "</p><hr>"
response.write "<p><b>CSRF Length</b>: " & len(replace(postedCSRF,"NA","")) & "</p>"
response.write "<p><b>Expected CSRF Length</b>: " & len(CSRF_token_max) & "</p><hr>"
response.write "<p><b>Posted Text</b>: " & Server.HTMLEncode(postedText) & "</p>"
%>
</td>
</tr>
<% end if %>
</tbody>
</table>
</form>
<script>
// Get the most recent CSRF token from the cookies
function getCSRF() {
document.getElementById("CSRF").value = getCookie("CSRF");
}
function getCookie(name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
}
</script>
<body>
</body>
</html>
You'll notice that as well as validating the CSRF token, I'm also checking the HTTP_REFERER against the servers domain name. Whenever a form is submitted the HTTP_REFERER header contains the URL of the website that submitted the post (this is issued by the browser and cannot be spoofed using a standard HTTP POST request). If the form wasn't submitted from your domain, then the form data won't be processed.
So at this point, it's now impossible for a third party website to use client side code to spam or manipulate a form that you host. This is because we're:
Requiring a valid token to be returned with each post request, and the token can only be issued and accessed on your website.
Checking the HTTP_REFERER header to make sure the post request was made from your website.
Using Access-Control-Allow-Origin to prevent javascript/ajax from posting to your form from a thrid party websites (javascript/ajax could potentially be used to spoof a fake HTTP_REFERER header)
However... if an attacker chooses to access your form using server-side code, then this opens up a whole can or worms. Browsers are required to abide by very strict rules, but when you take browsers out of the equation and use server-side code to access and manipulate a web form on another website, those rules can be bypassed. It would be possible for an attacker to use cURL (or a similar protocol) to request your web form, read the CSRF cookie issued by your server in the response headers, use that CSRF token to validate the form, post the form back to your server along with the CSRF cookie you issued AND spoof the HTTP_REFERER header making it appear as the form was submitted from your website. The attacker would have essentially created a bot, and these are very difficult to detect and prevent against, especially since they can spoof other headers too, such as the HTTP_USER_AGENT.
There are solutions to this though which use algorithms to detect suspicious activity. CloudFlare offers a really good bot management services that is great for detecting and blocking bots as well as general data scraping/mining: https://www.cloudflare.com/lp/bot-management/
I have two asp pages in the first page named verify.asp i have write this code:
verify.asp
<%
Username = Request.Form("loginx")
Password = Request.Form("passx")
liberado
Session("liberado") = Username
%>
in the second page i try to use the session variabel "liberado" with any result
barra.asp ,
<%
response.write(session("liberado"))
%>
What i'm making wrong? I m using chrome on IIS of windows 7, Username and Password have values
There was nothing really wrong with your code. Although I can see you've edited it now to remove the dim from liberado, but you've left liberado behind. This means your ASP will try and call a sub called liberado, which presumably doesn't exist. You can go ahead and remove that line.
<%
Dim Username, Password
Username = Request.Form("loginx")
Password = Request.Form("passx")
Session("liberado") = Username
%>
Trying to set a session whilst the session state is disabled will probably result in an error of some kind (and you didn't mention an error in your question). But make sure it's enabled by opening IIS and under ASP > Session Properties set "Enable Session State" to "True".
If it's already true then chances are there's something wrong with your form and the data isn't being posted. On your verify.asp page try running the following code:
for each item in request.form
response.write item & ": " & request.form(item) & "<br>"
next
This will output a list of all the form data being posted.
This could also be a cookie issue. If you're blocking cookies from being set in Chrome then there won't be an ASP session cookie, so session values won't be accessible as you move from page to page.
In Chrome press F12 to open developer tools, click the Applications tab, and from the "Cookies" drop down menu select your domain. Check there's an ASPSESSIONID cookie present, and it's the same cookie on both your ASP pages.
Check the application pool settings in IIS. If there are multiple worker processes active under "maximum worker processes", sessions don't always work. Sessions are stored per process, do if a different worker process handles the second request, the session from the first request might be missing. A setting of "0" means IIS uses as many processes as needed.
More information here
I have inherited an Classic ASP Site and a "bolt-on" ASP.NET site...
NEITHER are using Authentication, BOTH sides have a manual "reinvent-the- wheel" (hard-coded) security system that validates the user/pw from a SQL 2000 database (i.e. "if the user is found via a SQL SELECT, let them in").
New development is in ASP.NET... and they have "integrated" the two sites via ONE login (described above) on the Classic ASP side... then passing a GUID (saved at the time of login to the users record) they validate the GUID on the ASP.NET side ("yes, this is the correct GUID, therefore this is my user... let them in").
Up until now this has been working ONE DIRECTION (Classic ASP to ASP.NET) only with no issues.
(Getting to the problem, bear with me...)
Now they want to perform the same basic design from ASP.NET to Classic ASP by updating the GUID, passing it back, where the lookup validates the user, send them to the correct Classic ASP page... (so the user can return to the Classic ASP side without re-loging-in, previously required) HOWEVER...
***HERE's THE PROBLEM
Session("UserID") is used on the Classic ASP side to (hard code) validate the user... then Response.Redirect is run to send them back to the page that they previously left via "sRedirectToString" ...
'user is found in DB, so send them to the correct page...
Dim sRedirectToString = 'the correct url
Call Response.Redirect (sRedirectToString)
HOWEVER, Session("UserID") gets cleared by IIS or IE (dun'no) and the (hard-coded) validation fails because Session("UserID") is NULL (blank)
Here's the simple (only) validation:
If Trim(Session("UserID") & "") = "" Then
'Session timed out
Response.Redirect('the denied page)
Else
Response.Write "<meta http-equiv=""X-UA-Compatible"" content=""IE=EmulateIE7"">"
End If
So, why are the Session Variables being cleared by a Redirect? (there is no other system authentication is being used).
There is no Session.Abort, nor any specific coding that is clearing Session("UserID").
But when Session("UserID") is tested (see code above) it is found empty and redirects to the DENIED.asp page.
So, hoping there is some property like "PersistSessionVariables" (or something) that I can set so they don't clear...
BUT THEY DO INDEED CLEAR IMMEDIATELY AFTER THE REDIRECT AND THIS IS CONFUSING TO ME.
I appreciate all the Wizards help!
I'm writing some logging code that is based on SessionID...
However, when I log out (calling Session.Abandon), and log in once again, SessionID is still the same. Basically every browser on my PC has it's own session id "attached", and it won't change for some reason :/
Any ideas what is going on?
My Session config looks like this:
<sessionState
mode="InProc"
timeout="1" />
Thanks, Paweł
Check this article which explains the process on session.abandon
http://support.microsoft.com/kb/899918
Taken from above link -
"When you abandon a session, the session ID cookie is not removed from the browser of the user. Therefore, as soon as the session has been abandoned, any new requests to the same application will use the same session ID but will have a new session state instance"
This is a default behavior by design as stated here:
Session identifiers for abandoned or expired sessions are recycled by default. That is, if a request is made that includes the session identifier for an expired or abandoned session, a new session is started using the same session identifier. You can disable this by setting regenerateExpiredSessionId attribute of the sessionState configuration element to true
You can disable this setting as mentioned above.
EDIT: Setting regenerateExpiredSessionId attribute to true works only for cookieless sessions. To overcome your problem, you can consider to implement a custom class that inherits SessionIDManager class. You can get information about that here and here.
This is an old post but if someone is still looking for answers, here is a complete and step-by-step solution on how to achieve a clean logout with a new session ID every time.
Please note this article applies to cookie-enabled (cookieless=false) sites only.
Step (1) Modify your web.config file & add "regenerateExpiredSessionID" flag as under -
<sessionState mode="InProc" cookieless="false" regenerateExpiredSessionId="true" />
Step (2) Add the following code in your logout event -
Session.Clear();
Session.Abandon();
Response.Cookies.Add(New HttpCookie("ASP.NET_SessionId", ""));
Response.redirect(to you login page);
Step (3) Add the following code in your login page's page_load event -
if(!IsPostBack)
{
Session.Clear();
Session.Abandon();
}
Step 2 and 3 serve one IMPORTANT purpose. This code makes sure a brand new Session ID is generated after you click the "Login" button. This prevents Weak Session Management (Session Fixation vulnerability) which will likely be spotted during a 3rd party Penetration Testing of your site.
Hope this helps.
Here's what worked for me, the only caveat is that this code need to be separated from your login routine.
Response.Cookies("ASP.NET_SessionId").Expires = DateTime.Now.AddYears(-30)
It will not take effect until the page is finished loading. In my application I have a simple security routine, that forces a new ID, like this:
if session("UserID") = "" then
Response.Cookies("ASP.NET_SessionId").Expires = DateTime.Now.AddYears(-30)
Response.Redirect("login.aspx")
end if
You may explicitly clear the session cookie. You should control the cookie name by configuration and use same name while clearing.
Edit:
Clearing session cookie when session is abandoned will force ASP.NET to create new session & sessionid for next request. BTW, yet another way to clear the session cookie is to use SessionIDManager.RemoveSessionID method.
I'm developing a web application that uses an in-house SSO server for authentication. I have a link on my home page to a page called Logout.aspx. Logout.aspx clears the Forms Authentication cookie, all session data, then performs a redirect to the LoginUrl specified in the forms authentication configuration which is currently set to a page called Login.aspx.
However when Login.aspx loads, an attempt is made to implicitly reauthenticate the user against the SSO server using the SSO authentication ticket which was previously issued. If this ticket still exists, the previous user will be logged back in and sent back to the home page. I want to determine, when the Login page loads, whether the request has come via the Logout page. The UrlReferrer property of the request still references Home.aspx, presumably because this was the last url the client requested.
Currently I have a workaround in place whereby I append a querystring variable to the request from the logout page that instructs the Login page not to perform an implicit login and instead prompt the user for credentials. How can I determine programmatically whether the request came via a redirect from the Logout page?
Edit 29/04/2009:
Following the conversation with jellomonkey, I should point out that the interaction between the SSO server and the local forms authentication of the consuming website isn't directly relevant to the problem at hand. Expressed succinctly, my problem is:
User clicks HTML hyperlink from Home.aspx which takes them to Logout.aspx
Page_Load event handler of Logout.aspx clears Forms Authentication ticket and Session data and redirects the user to Login.aspx
Page_Load event of Login.aspx checks the UrlReferrer property of the Request object to determine whether the request came via the Logout page. However, in requests which have come via a redirect from Logout.aspx, the UrlReferrer property of the Request object is Home.aspx.
Why is this? Why is the UrlReferrer Home.aspx and not Logout.aspx?
The scenario you are describing should be working correctly unless the logout page is not actually deleting the forms authentication cookie. There are several ways to end the forms authentication session:
//I have seen instances where this does not work.
FormsAuthentication.SignOut()
//I have not seen this code fail before.
Dim cookie As HttpCookie = FormsAuthentication.GetAuthCookie( _
HttpContext.Current.User.Identity.Name, False)
cookie.Expires = Date.Now.AddDays(-1)
Response.Clear()
Response.AppendCookie(cookie)
Response.Redirect(FormsAuthentication.LoginUrl)
Also if you are using a role manager which stores in a cookie remember to call Roles.DeleteCookie().
Edit: In response to the updated question.
The Response.Redirect method does not return a header with a new URL referrer because the spec says that only client initiated requests should contain a referrer header. Here is the Response.Redirect code which you can see does not change the referrer header:
Public Sub Redirect(ByVal url As String, ByVal endResponse As Boolean)
If (url Is Nothing) Then
Throw New ArgumentNullException("url")
End If
If (url.IndexOf(ChrW(10)) >= 0) Then
Throw New ArgumentException(SR.GetString("Cannot_redirect_to_newline"))
End If
If Me._headersWritten Then
Throw New HttpException(SR.GetString("Cannot_redirect_after_headers_sent"))
End If
Dim handler As Page = TryCast(Me._context.Handler,Page)
If ((Not handler Is Nothing) AndAlso handler.IsCallback) Then
Throw New ApplicationException(SR.GetString("Redirect_not_allowed_in_callback"))
End If
url = Me.ApplyRedirectQueryStringIfRequired(url)
url = Me.ApplyAppPathModifier(url)
url = Me.ConvertToFullyQualifiedRedirectUrlIfRequired(url)
url = Me.UrlEncodeRedirect(url)
Me.Clear
If (((Not handler Is Nothing) AndAlso handler.IsPostBack) AndAlso (handler.SmartNavigation AndAlso (Me.Request.Item("__smartNavPostBack") = "true"))) Then
Me.Write("<BODY><ASP_SMARTNAV_RDIR url=""")
Me.Write(HttpUtility.HtmlEncode(url))
Me.Write("""></ASP_SMARTNAV_RDIR>")
Me.Write("</BODY>")
Else
Me.StatusCode = &H12E
Me.RedirectLocation = url
If ((url.StartsWith("http:", StringComparison.OrdinalIgnoreCase) OrElse url.StartsWith("https:", StringComparison.OrdinalIgnoreCase)) OrElse ((url.StartsWith("ftp:", StringComparison.OrdinalIgnoreCase) OrElse url.StartsWith("file:", StringComparison.OrdinalIgnoreCase)) OrElse url.StartsWith("news:", StringComparison.OrdinalIgnoreCase))) Then
url = HttpUtility.HtmlAttributeEncode(url)
Else
url = HttpUtility.HtmlAttributeEncode(HttpUtility.UrlEncode(url))
End If
Me.Write("<html><head><title>Object moved</title></head><body>" & ChrW(13) & ChrW(10))
Me.Write(("<h2>Object moved to here.</h2>" & ChrW(13) & ChrW(10)))
Me.Write("</body></html>" & ChrW(13) & ChrW(10))
End If
Me._isRequestBeingRedirected = True
If endResponse Then
Me.End
End If
End Sub
You can use reflector to follow the other methods but I don't see one which changes any header.
Response.Redirect("login.aspx?from=logout")
Steve Yates
ITS, Inc.
Why doesn't Tarzan have a beard?
~ Taglines by Taglinator: www.srtware.com ~