mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1208629 - Properly support data: and blob: URIs with an integrity atribute. r=ckerschb
This commit is contained in:
parent
97f026f94b
commit
7e25502ebc
@ -1425,16 +1425,9 @@ nsScriptLoader::OnStreamComplete(nsIStreamLoader* aLoader,
|
||||
NS_ASSERTION(request, "null request in stream complete handler");
|
||||
NS_ENSURE_TRUE(request, NS_ERROR_FAILURE);
|
||||
|
||||
nsCOMPtr<nsIHttpChannel> httpChannel;
|
||||
{
|
||||
nsCOMPtr<nsIRequest> req;
|
||||
aLoader->GetRequest(getter_AddRefs(req));
|
||||
httpChannel = do_QueryInterface(req);
|
||||
} // throw away req, we only need the channel
|
||||
|
||||
nsresult rv = NS_ERROR_SRI_CORRUPT;
|
||||
if (request->mIntegrity.IsEmpty() ||
|
||||
NS_SUCCEEDED(SRICheck::VerifyIntegrity(request->mIntegrity, httpChannel,
|
||||
NS_SUCCEEDED(SRICheck::VerifyIntegrity(request->mIntegrity, aLoader,
|
||||
request->mCORSMode, aStringLen,
|
||||
aString, mDocument))) {
|
||||
rv = PrepareLoadedRequest(request, aLoader, aStatus, aStringLen, aString);
|
||||
|
@ -16,6 +16,8 @@
|
||||
#include "nsIProtocolHandler.h"
|
||||
#include "nsIScriptError.h"
|
||||
#include "nsIScriptSecurityManager.h"
|
||||
#include "nsIStreamLoader.h"
|
||||
#include "nsIUnicharStreamLoader.h"
|
||||
#include "nsIURI.h"
|
||||
#include "nsNetUtil.h"
|
||||
#include "nsWhitespaceTokenizer.h"
|
||||
@ -45,9 +47,13 @@ static nsresult
|
||||
IsEligible(nsIChannel* aChannel, const CORSMode aCORSMode,
|
||||
const nsIDocument* aDocument)
|
||||
{
|
||||
NS_ENSURE_ARG_POINTER(aChannel);
|
||||
NS_ENSURE_ARG_POINTER(aDocument);
|
||||
|
||||
if (!aChannel) {
|
||||
SRILOG(("SRICheck::IsEligible, null channel"));
|
||||
return NS_ERROR_SRI_NOT_ELIGIBLE;
|
||||
}
|
||||
|
||||
// Was the sub-resource loaded via CORS?
|
||||
if (aCORSMode != CORS_NONE) {
|
||||
SRILOG(("SRICheck::IsEligible, CORS mode"));
|
||||
@ -236,38 +242,14 @@ SRICheck::IntegrityMetadata(const nsAString& aMetadataList,
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
/* static */ nsresult
|
||||
SRICheck::VerifyIntegrity(const SRIMetadata& aMetadata,
|
||||
nsIChannel* aChannel,
|
||||
const CORSMode aCORSMode,
|
||||
const nsAString& aString,
|
||||
const nsIDocument* aDocument)
|
||||
static nsresult
|
||||
VerifyIntegrityInternal(const SRIMetadata& aMetadata,
|
||||
nsIChannel* aChannel,
|
||||
const CORSMode aCORSMode,
|
||||
uint32_t aStringLen,
|
||||
const uint8_t* aString,
|
||||
const nsIDocument* aDocument)
|
||||
{
|
||||
NS_ConvertUTF16toUTF8 utf8Hash(aString);
|
||||
return VerifyIntegrity(aMetadata, aChannel, aCORSMode, utf8Hash.Length(),
|
||||
(uint8_t*)utf8Hash.get(), aDocument);
|
||||
}
|
||||
|
||||
/* static */ nsresult
|
||||
SRICheck::VerifyIntegrity(const SRIMetadata& aMetadata,
|
||||
nsIChannel* aChannel,
|
||||
const CORSMode aCORSMode,
|
||||
uint32_t aStringLen,
|
||||
const uint8_t* aString,
|
||||
const nsIDocument* aDocument)
|
||||
{
|
||||
if (MOZ_LOG_TEST(GetSriLog(), mozilla::LogLevel::Debug)) {
|
||||
nsAutoCString requestURL;
|
||||
nsCOMPtr<nsIURI> originalURI;
|
||||
if (NS_SUCCEEDED(aChannel->GetOriginalURI(getter_AddRefs(originalURI))) &&
|
||||
originalURI) {
|
||||
originalURI->GetAsciiSpec(requestURL);
|
||||
// requestURL will be empty if GetAsciiSpec fails
|
||||
}
|
||||
SRILOG(("SRICheck::VerifyIntegrity, url=%s (length=%u)",
|
||||
requestURL.get(), aStringLen));
|
||||
}
|
||||
|
||||
MOZ_ASSERT(!aMetadata.IsEmpty()); // should be checked by caller
|
||||
|
||||
// IntegrityMetadata() checks this and returns "no metadata" if
|
||||
@ -306,5 +288,62 @@ SRICheck::VerifyIntegrity(const SRIMetadata& aMetadata,
|
||||
return NS_ERROR_SRI_CORRUPT;
|
||||
}
|
||||
|
||||
/* static */ nsresult
|
||||
SRICheck::VerifyIntegrity(const SRIMetadata& aMetadata,
|
||||
nsIUnicharStreamLoader* aLoader,
|
||||
const CORSMode aCORSMode,
|
||||
const nsAString& aString,
|
||||
const nsIDocument* aDocument)
|
||||
{
|
||||
NS_ENSURE_ARG_POINTER(aLoader);
|
||||
|
||||
NS_ConvertUTF16toUTF8 utf8Hash(aString);
|
||||
nsCOMPtr<nsIChannel> channel;
|
||||
aLoader->GetChannel(getter_AddRefs(channel));
|
||||
|
||||
if (MOZ_LOG_TEST(GetSriLog(), mozilla::LogLevel::Debug)) {
|
||||
nsAutoCString requestURL;
|
||||
nsCOMPtr<nsIURI> originalURI;
|
||||
if (channel &&
|
||||
NS_SUCCEEDED(channel->GetOriginalURI(getter_AddRefs(originalURI))) &&
|
||||
originalURI) {
|
||||
originalURI->GetAsciiSpec(requestURL);
|
||||
}
|
||||
SRILOG(("SRICheck::VerifyIntegrity (unichar stream), url=%s (length=%u)",
|
||||
requestURL.get(), utf8Hash.Length()));
|
||||
}
|
||||
|
||||
return VerifyIntegrityInternal(aMetadata, channel, aCORSMode,
|
||||
utf8Hash.Length(), (uint8_t*)utf8Hash.get(),
|
||||
aDocument);
|
||||
}
|
||||
|
||||
/* static */ nsresult
|
||||
SRICheck::VerifyIntegrity(const SRIMetadata& aMetadata,
|
||||
nsIStreamLoader* aLoader,
|
||||
const CORSMode aCORSMode,
|
||||
uint32_t aStringLen,
|
||||
const uint8_t* aString,
|
||||
const nsIDocument* aDocument)
|
||||
{
|
||||
NS_ENSURE_ARG_POINTER(aLoader);
|
||||
|
||||
nsCOMPtr<nsIRequest> request;
|
||||
aLoader->GetRequest(getter_AddRefs(request));
|
||||
NS_ENSURE_ARG_POINTER(request);
|
||||
nsCOMPtr<nsIChannel> channel;
|
||||
channel = do_QueryInterface(request);
|
||||
|
||||
if (MOZ_LOG_TEST(GetSriLog(), mozilla::LogLevel::Debug)) {
|
||||
nsAutoCString requestURL;
|
||||
request->GetName(requestURL);
|
||||
SRILOG(("SRICheck::VerifyIntegrity (stream), url=%s (length=%u)",
|
||||
requestURL.get(), aStringLen));
|
||||
}
|
||||
|
||||
return VerifyIntegrityInternal(aMetadata, channel, aCORSMode,
|
||||
aStringLen, aString, aDocument);
|
||||
}
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
@ -11,11 +11,9 @@
|
||||
#include "nsCOMPtr.h"
|
||||
#include "SRIMetadata.h"
|
||||
|
||||
class nsIChannel;
|
||||
class nsIDocument;
|
||||
class nsIScriptSecurityManager;
|
||||
class nsIStreamLoader;
|
||||
class nsIURI;
|
||||
class nsIUnicharStreamLoader;
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
@ -39,7 +37,7 @@ public:
|
||||
* must prevent the resource from loading.
|
||||
*/
|
||||
static nsresult VerifyIntegrity(const SRIMetadata& aMetadata,
|
||||
nsIChannel* aChannel,
|
||||
nsIUnicharStreamLoader* aLoader,
|
||||
const CORSMode aCORSMode,
|
||||
const nsAString& aString,
|
||||
const nsIDocument* aDocument);
|
||||
@ -49,7 +47,7 @@ public:
|
||||
* must prevent the resource from loading.
|
||||
*/
|
||||
static nsresult VerifyIntegrity(const SRIMetadata& aMetadata,
|
||||
nsIChannel* aChannel,
|
||||
nsIStreamLoader* aLoader,
|
||||
const CORSMode aCORSMode,
|
||||
uint32_t aStringLen,
|
||||
const uint8_t* aString,
|
||||
|
@ -58,6 +58,19 @@
|
||||
ok(false, "Non-CORS loads with correct hashes redirecting to a different origin should be blocked!");
|
||||
}
|
||||
|
||||
function good_correctDataBlocked() {
|
||||
ok(true, "A data: URL was blocked correctly.");
|
||||
}
|
||||
function bad_correctDataLoaded() {
|
||||
ok(false, "Since data: URLs are neither same-origin nor CORS, they should be blocked!");
|
||||
}
|
||||
function good_correctDataCORSBlocked() {
|
||||
ok(true, "A data: URL was blocked correctly even though it was a CORS load.");
|
||||
}
|
||||
function bad_correctDataCORSLoaded() {
|
||||
todo(false, "We should not load scripts in data: URIs regardless of CORS mode!");
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
SimpleTest.finish()
|
||||
}
|
||||
@ -99,6 +112,19 @@
|
||||
onerror="good_correct301Blocked()"
|
||||
onload="bad_correct301Loaded()"></script>
|
||||
|
||||
<!-- data: URLs are not same-origin -->
|
||||
<script src="data:,console.log('data:valid');"
|
||||
integrity="sha256-W5I4VIN+mCwOfR9kDbvWoY1UOVRXIh4mKRN0Nz0ookg="
|
||||
onerror="good_correctDataBlocked()"
|
||||
onload="bad_correctDataLoaded()"></script>
|
||||
|
||||
<!-- data: URLs should always be opaque -->
|
||||
<script src="data:,console.log('data:valid');"
|
||||
crossorigin="anonymous"
|
||||
integrity="sha256-W5I4VIN+mCwOfR9kDbvWoY1UOVRXIh4mKRN0Nz0ookg="
|
||||
onerror="good_correctDataCORSBlocked()"
|
||||
onload="bad_correctDataCORSLoaded()"></script>
|
||||
|
||||
<script>
|
||||
ok(window.hasCORSLoaded, "CORS-enabled resource with a correct hash");
|
||||
ok(!window.hasNonCORSLoaded, "Correct hash, but non-CORS, should be blocked");
|
||||
|
@ -102,7 +102,21 @@
|
||||
function bad_invalid302Loaded() {
|
||||
ok(false, "We should not load scripts with a 302 response and the wrong hash!");
|
||||
}
|
||||
</script>
|
||||
|
||||
function good_validBlobLoaded() {
|
||||
ok(true, "A script was loaded successfully from a blob: URL.");
|
||||
}
|
||||
function bad_validBlobBlocked() {
|
||||
ok(false, "We should load scripts using blob: URLs with the right hash!");
|
||||
}
|
||||
|
||||
function good_invalidBlobBlocked() {
|
||||
ok(true, "A script was blocked successfully from a blob: URL.");
|
||||
}
|
||||
function bad_invalidBlobLoaded() {
|
||||
ok(false, "We should not load scripts using blob: URLs with the wrong hash!");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- valid hash. should trigger onload -->
|
||||
@ -200,6 +214,32 @@
|
||||
onerror="good_invalid302Blocked()"
|
||||
onload="bad_invalid302Loaded()"></script>
|
||||
|
||||
<!-- valid sha256 for a blob: URL -->
|
||||
<script>
|
||||
var blob = new Blob(["console.log('blob:valid');"],
|
||||
{type:"application/javascript"});
|
||||
var script = document.createElement('script');
|
||||
script.setAttribute('src', URL.createObjectURL(blob));
|
||||
script.setAttribute('integrity', 'sha256-AwLdXiGfCqOxOXDPUim73G8NVEL34jT0IcQR/tqv/GQ=');
|
||||
script.onerror = bad_validBlobBlocked;
|
||||
script.onload = good_validBlobLoaded;
|
||||
var head = document.getElementsByTagName('head').item(0);
|
||||
head.appendChild(script);
|
||||
</script>
|
||||
|
||||
<!-- invalid sha256 for a blob: URL -->
|
||||
<script>
|
||||
var blob = new Blob(["console.log('blob:invalid');"],
|
||||
{type:"application/javascript"});
|
||||
var script = document.createElement('script');
|
||||
script.setAttribute('src', URL.createObjectURL(blob));
|
||||
script.setAttribute('integrity', 'sha256-AwLdXiGfCqOxOXDPUim73G8NVEL34jT0IcQR/tqv/GQ=');
|
||||
script.onerror = good_invalidBlobBlocked;
|
||||
script.onload = bad_invalidBlobLoaded;
|
||||
var head = document.getElementsByTagName('head').item(0);
|
||||
head.appendChild(script);
|
||||
</script>
|
||||
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
</div>
|
||||
|
@ -6,12 +6,28 @@
|
||||
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
<script type="application/javascript">
|
||||
function check_styles() {
|
||||
var redText = document.getElementById('red-text');
|
||||
var blackText = document.getElementById('black-text');
|
||||
var redTextColor = window.getComputedStyle(redText, null).getPropertyValue('color');
|
||||
var blackTextColor = window.getComputedStyle(blackText, null).getPropertyValue('color');
|
||||
ok(redTextColor == 'rgb(255, 0, 0)', "The first part should be red.");
|
||||
todo(blackTextColor == 'rgb(0, 0, 0)', "The second part should still be black.");
|
||||
}
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
window.onload = function() {
|
||||
check_styles();
|
||||
SimpleTest.finish();
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
function good_correctHashCORSLoaded() {
|
||||
ok(true, "A CORS cross-domain stylesheet with correct hash was correctly loaded.");
|
||||
}
|
||||
function bad_correctHashCORSBlocked() {
|
||||
ok(false, "We should load CORS cross-domain stylesheets with hashes that match!");
|
||||
}
|
||||
function good_correctHashBlocked() {
|
||||
ok(true, "A non-CORS cross-domain stylesheet with correct hash was correctly blocked.");
|
||||
}
|
||||
@ -26,22 +42,55 @@
|
||||
ok(false, "We should load non-CORS cross-domain stylesheets with incorrect hashes!");
|
||||
}
|
||||
|
||||
function good_correctDataBlocked() {
|
||||
ok(true, "A stylesheet was correctly blocked, because it came from a data: URI.");
|
||||
}
|
||||
function bad_correctDataLoaded() {
|
||||
ok(false, "We should not load stylesheets in data: URIs!");
|
||||
}
|
||||
function good_correctDataCORSBlocked() {
|
||||
ok(true, "A stylesheet was correctly blocked, because it came from a data: URI even though it was a CORS load.");
|
||||
}
|
||||
function bad_correctDataCORSLoaded() {
|
||||
todo(false, "We should not load stylesheets in data: URIs regardless of CORS mode!");
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- valid non-CORS sha256 hash. should trigger onload -->
|
||||
<!-- valid CORS sha256 hash -->
|
||||
<link rel="stylesheet" href="http://example.com/tests/dom/security/test/sri/style1.css"
|
||||
crossorigin="anonymous"
|
||||
integrity="sha256-qs8lnkunWoVldk5d5E+652yth4VTSHohlBKQvvgGwa8="
|
||||
onerror="bad_correctHashCORSBlocked()"
|
||||
onload="good_correctHashCORSLoaded()">
|
||||
|
||||
<!-- valid non-CORS sha256 hash -->
|
||||
<link rel="stylesheet" href="style_301.css"
|
||||
integrity="sha256-qs8lnkunWoVldk5d5E+652yth4VTSHohlBKQvvgGwa8="
|
||||
onerror="good_correctHashBlocked()"
|
||||
onload="bad_correctHashLoaded()">
|
||||
|
||||
<!-- invalid non-CORS sha256 hash. should trigger onload -->
|
||||
<!-- invalid non-CORS sha256 hash -->
|
||||
<link rel="stylesheet" href="style_301.css?again"
|
||||
integrity="sha256-bogus"
|
||||
onerror="good_incorrectHashBlocked()"
|
||||
onload="bad_incorrectHashLoaded()">
|
||||
|
||||
<!-- valid non-CORS sha256 hash in a data: URL -->
|
||||
<link rel="stylesheet" href="data:text/css,.red-text{color:red}"
|
||||
integrity="sha256-ewUcnAs4+XY5k2JpfUQGFdG5YMZkq80/nIKW67kd7vE="
|
||||
onerror="good_correctDataBlocked()"
|
||||
onload="bad_correctDataLoaded()">
|
||||
|
||||
<!-- valid CORS sha256 hash in a data: URL -->
|
||||
<link rel="stylesheet" href="data:text/css,.red-text{color:red}"
|
||||
crossorigin="anonymous"
|
||||
integrity="sha256-ewUcnAs4+XY5k2JpfUQGFdG5YMZkq80/nIKW67kd7vE="
|
||||
onerror="good_correctDataCORSBlocked()"
|
||||
onload="bad_correctDataCORSLoaded()">
|
||||
</head>
|
||||
<body>
|
||||
<p><span id="red-text">This should be red.</span></p>
|
||||
<p><span id="red-text">This should be red</span> but
|
||||
<span id="black-text" class="red-text">this should remain black.</span></p>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
</div>
|
||||
|
@ -8,11 +8,17 @@
|
||||
<script type="application/javascript">
|
||||
function check_styles() {
|
||||
var redText = document.getElementById('red-text');
|
||||
var blackText = document.getElementById('black-text');
|
||||
var blueText = document.getElementById('blue-text-element');
|
||||
var blackText1 = document.getElementById('black-text');
|
||||
var blackText2 = document.getElementById('black-text-2');
|
||||
var redTextColor = window.getComputedStyle(redText, null).getPropertyValue('color');
|
||||
var blackTextColor = window.getComputedStyle(blackText, null).getPropertyValue('color');
|
||||
var blueTextColor = window.getComputedStyle(blueText, null).getPropertyValue('color');
|
||||
var blackTextColor1 = window.getComputedStyle(blackText1, null).getPropertyValue('color');
|
||||
var blackTextColor2 = window.getComputedStyle(blackText2, null).getPropertyValue('color');
|
||||
ok(redTextColor == 'rgb(255, 0, 0)', "The first part should be red.");
|
||||
ok(blackTextColor == 'rgb(0, 0, 0)', "The second part should still be black.");
|
||||
ok(blueTextColor == 'rgb(0, 0, 255)', "The second part should be blue.");
|
||||
ok(blackTextColor1 == 'rgb(0, 0, 0)', "The second last part should still be black.");
|
||||
ok(blackTextColor2 == 'rgb(0, 0, 0)', "The last part should still be black.");
|
||||
}
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
@ -42,6 +48,19 @@
|
||||
function bad_incorrectHashLoaded() {
|
||||
ok(false, "We should not load stylesheets with hashes that do not match the content!");
|
||||
}
|
||||
|
||||
function good_validBlobLoaded() {
|
||||
ok(true, "A stylesheet was loaded successfully from a blob: URL with the right hash.");
|
||||
}
|
||||
function bad_validBlobBlocked() {
|
||||
ok(false, "We should load stylesheets using blob: URLs with the right hash!");
|
||||
}
|
||||
function good_invalidBlobBlocked() {
|
||||
ok(true, "A stylesheet was blocked successfully from a blob: URL with an invalid hash.");
|
||||
}
|
||||
function bad_invalidBlobLoaded() {
|
||||
ok(false, "We should not load stylesheets using blob: URLs when they have the wrong hash!");
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- valid sha256 hash. should trigger onload -->
|
||||
@ -63,8 +82,39 @@
|
||||
onload="bad_incorrectHashLoaded()">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- valid sha256 for a blob: URL -->
|
||||
<script>
|
||||
var blob = new Blob(['.blue-text{color:blue}'],
|
||||
{type: 'text/css'});
|
||||
var link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.setAttribute('integrity', 'sha256-/F+EMVnTWYJOAzN5n7/21idiydu6nRi33LZOISZtwOM=');
|
||||
link.onerror = bad_validBlobBlocked;
|
||||
link.onload = good_validBlobLoaded;
|
||||
document.body.appendChild(link);
|
||||
</script>
|
||||
|
||||
<!-- invalid sha256 for a blob: URL -->
|
||||
<script>
|
||||
var blob = new Blob(['.black-text{color:blue}'],
|
||||
{type: 'text/css'});
|
||||
var link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.setAttribute('integrity', 'sha256-/F+EMVnTWYJOAzN5n7/21idiydu6nRi33LZOISZtwOM=');
|
||||
link.onerror = good_invalidBlobBlocked;
|
||||
link.onload = bad_invalidBlobLoaded;
|
||||
document.body.appendChild(link);
|
||||
</script>
|
||||
|
||||
<p><span id="red-text">This should be red </span> and
|
||||
<span id="black-text">this should stay black.</p>
|
||||
<span class="blue-text" id="blue-text-element">this should be blue.</span>
|
||||
However, <span id="black-text">this should stay black</span> and
|
||||
<span class="black-text" id="black-text-2">this should also stay black.</span>
|
||||
</p>
|
||||
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
</div>
|
||||
|
@ -23,6 +23,7 @@ support-files =
|
||||
script_401.js
|
||||
script_401.js^headers^
|
||||
style1.css
|
||||
style1.css^headers^
|
||||
style2.css
|
||||
style3.css
|
||||
style_301.css
|
||||
|
1
dom/security/test/sri/style1.css^headers^
Normal file
1
dom/security/test/sri/style1.css^headers^
Normal file
@ -0,0 +1 @@
|
||||
Access-Control-Allow-Origin: http://mochi.test:8888
|
@ -965,7 +965,7 @@ SheetLoadData::OnStreamComplete(nsIUnicharStreamLoader* aLoader,
|
||||
|
||||
SRIMetadata sriMetadata = mSheet->GetIntegrity();
|
||||
if (!sriMetadata.IsEmpty() &&
|
||||
NS_FAILED(SRICheck::VerifyIntegrity(sriMetadata, httpChannel,
|
||||
NS_FAILED(SRICheck::VerifyIntegrity(sriMetadata, aLoader,
|
||||
mSheet->GetCORSMode(), aBuffer,
|
||||
mLoader->mDocument))) {
|
||||
LOG((" Load was blocked by SRI"));
|
||||
|
Loading…
Reference in New Issue
Block a user