When you setup SQL Server Reporting Services [SSRS] in SharePoint Integrated mode, browsing http://servername/ReportServer will show you a directory listing view of SharePoint Site Collections that is security trimmed to you. However, there are some downsides to this view. Users can probe through the directory structure of your site that they have read access to, and users may see links to sites that you didn’t intend for them to see.
In my test farm, when a user named BobSmith browses, he sees the following:
If bobsmith clicks on the link for http://teams.jormanvmas.com, he is able to browse through the folder structure of the site collection. If he ever finds a report [RDL file], he could click on the RDL file and the report would try to render. Users may also discover URLs that they didn’t know existed, and the person that owns the site may not want them to know the site exists.
For example, in the above output, BobSmith sees a link for http://teams.jormanvmas.com/sites/cqwp. However, if he clicks that link he gets:
If he browses the URL directly in the browser, as opposed to going through Report Server, he gets the following:
This is odd, the Report Server page should return sites that BobSmith has permissions to, but for some reason, he’s unable to browse the URL returned. To track down why this is occurring, I turned to the Debugging Tools for Windows. I attached windbg to ReportingServicesService.exe, and broke in on managed exceptions [command : sxe clr]. This revealed that Report Services has an HttpHandler, that eventually calls OpenWeb for each site collection in the farm.
Microsoft.SharePoint.Library.SPRequest.OpenWeb(…) Microsoft.SharePoint.SPWeb.InitWeb() Microsoft.SharePoint.SPWeb.get_Description() Microsoft.ReportingServices.SharePoint.Objects.RSSPImpWeb.get_Description() Microsoft.ReportingServices.SharePoint.Utilities.CatalogItemUtilities.CreateCatalogItem(Microsoft.ReportingServices.SharePoint.ObjectModel.RSSPWeb) Microsoft.ReportingServices.SharePoint.Server.SharePointDBInterface.InternalFindObjects(…) Microsoft.ReportingServices.SharePoint.Server.SharePointDBInterface.FindObjectsNonRecursive(…) Microsoft.ReportingServices.Library.ListChildrenAction.PerformActionNow() Microsoft.ReportingServices.Library.RSSoapAction`1[[System.__Canon, mscorlib]].Execute() Microsoft.ReportingServices.WebServer.ReportServiceHttpHandler.RenderFolder() Microsoft.ReportingServices.WebServer.ReportServiceHttpHandler.RenderPageContent() Microsoft.ReportingServices.WebServer.ReportServiceHttpHandler.RenderPage() Microsoft.ReportingServices.WebServer.ReportServiceHttpHandler.ProcessRequest(System.Web.HttpContext)
I confirmed that when this call fails, the link will not render. This means that BobSmith’s account is able to call OpenWeb on the Site, even though BobSmith is unable to browse the site. I then browsed the CQWP site as another account that has full access, and used the Check Permissions functionality to determine what permissions BobSmith has on the site. This showed that BobSmith has Limited Access, which was granted via the Style Resource Readers group.
I then checked the Limited Access Permission Set and found that the following are granted:
There we are, the link shows up because BobSmith has Open permissions, which is enough for Reporting Server to render the link. I wanted to confirm why BobSmith was in the Style Resource Readers group. This group is added by the Publishing Features, and the group contains the NT Authority\Authenticated Users group. For some information on Style Resource Readers group and the authenticated users group, see the following article http://technet.microsoft.com/en-us/library/cc262690.aspx
From the above, we know that any site with the Publishing Features enabled will show up in the list of links when a user browses /ReportServer/, and if a user has read access, they can probe into the directory structure of your site. There’s not an out of the box way to disable that view, so if you want to disable it, you’re looking at writing some code. I put together an HttpModule that will block incoming requests that will render that view. The trick is that the Report Viewer control also makes requests to the /ReportServer URL to render reports, so you can’t block all requests. I went with the following to determine if a request should be blocked:
The first scenario covers users sending a ListChildren command to the ReportServer. The second scenario covers a user sending a GET to the ReportServer url in a browser, as well as users sending a POST request via PowerShell or some other language to the root. These filters will still allow other rs:Command values as well as calls to the ASMX web services in the /ReportServer URL.
Here’s the code for a sample HttpModule. There are 3 methods:
1: using System;
2: using System.Web;
4: /// <summary>
5: /// HttpModule that checks incoming URLs and blocks HTTP GET requests to /ReportServer or /ReportServer/. This
6: /// prevents users from browsing the Report Server URL to click through links to try and find reports.
9: /// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
10: /// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
11: /// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
12: /// PARTICULAR PURPOSE.
13: /// </summary>
16: namespace Sample.SharePoint.ReportServer
18: public class ReportServerBlocker : IHttpModule
20: public void ReportServer()
24: public void Dispose()
29: //When the module is initialized, hook the BeginRequest event
30: public void Init(HttpApplication context)
32: context.BeginRequest += new EventHandler(this.Application_BeginRequest);
35: //When a new request comes in, check the URL. If the request is a GET request to /ReportServer or /ReportServer/, then clear and end the response.
36: private void Application_BeginRequest(Object source, EventArgs e)
38: HttpApplication application = (HttpApplication)source;
39: HttpContext context = application.Context;
41: //Pull the HttpMethod and the URL from the request
42: string httpMethod = context.Request.HttpMethod.ToLower();
43: string path = context.Request.Url.AbsolutePath.ToLower();
45: //Pull the rs:Command querystring
46: string command = context.Request.QueryString["rs:Command"];
48: //if the rs:Command querystring has ListChildren, fail out the request
49: if (command != null)
51: if (command.ToLower() == "listchildren")
60: //if there is no querystring, and the request is to /reportserver or /reportserver/, fail the request
61: else if (command == null && (path.EndsWith("reportserver") || path.EndsWith("reportserver/")))
67: private void FailRequest(HttpContext context)
Once you build out the HttpModule, you add it to the <httpModules> section of the web.config for Report Server, then restart the SQL Server Reporting Services service. By default, the web.config is found at :
C:\Program Files\Microsoft SQL Server\MSRS10_50.MSSQLSERVER\Reporting Services\ReportServer\web.config
For example, if you build the above code into an assembly named Sample.SharePoint.ReportServer.dll, you would add the following to the <httpModules> section of the web.config [after the <clear/> tag]:
<add name="ReportViewerBlocker" type="Sample.SharePoint.ReportServer.ReportServerBlocker, Sample.SharePoint.ReportServer, Version=220.127.116.11, Culture=neutral, PublicKeyToken=263da74b91424116" />
nice one i see myself using this in the future since I am deploying SSRS in SP2010.
Thanks for posting this solution - this works well