OWASP O2 Platform Blog

Fixing one of JPetStore’s AutoBinding Vulnerabilities (changing the purchase price)

JPetStore (test application included with the Spring Framework) has a couple Autobinding vulnerabilities. The objective of this post is to present a solution for fixing them that meets the following criteria:

  • minumal code changes on the controllers
  • no code changes on the views
  • dramatically reduction of the size of the binded object (the ModelAttribute, CommandClass),
  • matches the expected web-layer inputs (i.e. the attack surface)
  • supports UnitTesting
  • doesn’t rely on Spring Framework’s recommended solution (which is based on the SetAllowedFields and SetDisallowedFields)

Here are a number of posts that give the brackground to this problem and document the O2 Script I’m using to test the fix:

The vulnerability we are going to fix is the one that allows (amongst other things) the modification of the TotalPrice value of our shopping cart (which is not a good thing :) )

This vulnerability is graphically described in this video:

… and it is fundamentally created by the fact that the setTotalPrice(…) method is exposed  in the Order class which is exposed in the OrderForm class (which is used as the command class for the OrderFormController).

So here is the fix.

1) The first step is to create a class that can be used on as the CommandClass. Trying to give it a name that made sense for what is was going to do, I called it OrderData and place it in a package called …jpetstore.domain.fromWeb (vs the original OrderForm class in the package … jpetstore.domain)

package org.springframework.samples.jpetstore.domain.fromWeb;
import org.springframework.samples.jpetstore.domain.Order;
import org.springframework.samples.jpetstore.web.spring.OrderForm;

public class OrderData {

private OrderForm orderForm;

public OrderData(OrderForm orderForm)
{
this.orderForm = orderForm;
}

public static OrderForm getOrderForm(Object command)
{
OrderData orderData = (OrderData)command;
return orderData.orderForm;
}
//bindable values
public OrderData getOrder()
{
return this;
}

....

}

This expects to receive an OrderForm object in its contructor which it will store in a private field.

The static getOrderForm() method is there to help the casting of the object given to us by the Spring Framework

2) On the FormBackingObject method return an OrderData object instead of an OrderForm object (which is vulnerable to the AutoBinding injection) and put on the Http Request attributes the original orderForm  object (this will allow the views to have access to the original orderForm , which they expec,t, and prevent us from needing to make code changes to the view (the fact that views should have access to massive domain objects is a topic for another time))

protected Object formBackingObject(HttpServletRequest request) throws ModelAndViewDefiningException {


UserSession userSession = (UserSession) request.getSession().getAttribute("userSession");
Cart cart = (Cart) request.getSession().getAttribute("sessionCart");
if (cart != null) {
// Re-read account from DB at team's request.
Account account = this.petStore.getAccount(userSession.getAccount().getUsername());
OrderForm orderForm = new OrderForm();
orderForm.getOrder().initOrder(account, cart);

request.setAttribute("orderForm", orderForm);     // AutoBinding fix
return new OrderData(orderForm);                // AutoBinding fix
}
else {
ModelAndView modelAndView = new ModelAndView("Error");
modelAndView.addObject("message", "An order could not be created because a cart could not be found.");
throw new ModelAndViewDefiningException(modelAndView);
}
}

3) modify all locations that originally received the OrderFrom object from SpringFramework in order to handle the fact that it is now an OrderData object (this uses the static method getOrderForm in order to make the code change short and sweet)

> onBindAndValidate

protected void onBindAndValidate(HttpServletRequest request, Object command, BindException errors, int page) {
if (page == 0 && request.getParameter("shippingAddressRequired") == null) {

//OrderForm orderForm = (OrderForm) command;            // AutoBinding fix
OrderForm orderForm = OrderData.getOrderForm(command);     // AutoBinding fix
orderForm.setShippingAddressRequired(false);
}
}

> on getTargetPage:

protected int getTargetPage(HttpServletRequest request, Object command, Errors errors, int currentPage) {
//OrderForm orderForm = (OrderForm) command;                    // AutoBinding fix
OrderForm orderForm = OrderData.getOrderForm(command);            // AutoBinding fix
if (currentPage == 0 && orderForm.isShippingAddressRequired()) {
return 1;
}
else {
return 2;
}
}

>processFinish

protected ModelAndView processFinish(
HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) {
//OrderForm orderForm = (OrderForm) command;                    // AutoBinding fix
OrderForm orderForm = OrderData.getOrderForm(command);            // AutoBinding fix
this.petStore.insertOrder(orderForm.getOrder());
request.getSession().removeAttribute("sessionCart");
Map model = new HashMap();
model.put("order", orderForm.getOrder());
model.put("message", "Thank you, your order has been submitted.");
return new ModelAndView("ViewOrder", model);
}

4) finally add to the FormData object the getters and setters of the variables that we want to expose.

A good place to find out which variables to expose is to look at the values that are submited from the web pages, in this case:

Here is the full code of this class:

package org.springframework.samples.jpetstore.domain.fromWeb;
import org.springframework.samples.jpetstore.domain.Order;
import org.springframework.samples.jpetstore.web.spring.OrderForm;

public class OrderData {

private OrderForm orderForm;

public OrderData(OrderForm orderForm)
{
this.orderForm = orderForm;
}

public static OrderForm getOrderForm(Object command)
{
OrderData orderData = (OrderData)command;
return orderData.orderForm;
}

//bindable values
public void setShippingAddressRequired    (boolean shippingAddressRequired)
{
this.orderForm.setShippingAddressRequired(shippingAddressRequired);
}

public OrderData getOrder()
{
return this;
}

public void setCardType            (String cardType)             { this.orderForm.getOrder().setCardType            (cardType);}
public void setCreditCard        (String creditCard)         { this.orderForm.getOrder().setCreditCard        (creditCard); }
public void setExpiryDate        (String expiryDate)            { this.orderForm.getOrder().setExpiryDate        (expiryDate); }
public void setBillToFirstName    (String billToFirstName)     { this.orderForm.getOrder().setBillToFirstName    (billToFirstName); }
public void setBillToLastName    (String billToLastName)     { this.orderForm.getOrder().setBillToLastName    (billToLastName); }
public void setBillAddress1        (String billAddress1)         { this.orderForm.getOrder().setBillAddress1        (billAddress1); }
public void setBillAddress2        (String billAddress1)         { this.orderForm.getOrder().setBillAddress2        (billAddress1); }
public void setBillCity            (String billCity)             { this.orderForm.getOrder().setBillCity            (billCity); }
public void setBillState        (String billState)             { this.orderForm.getOrder().setBillState        (billState); }
public void setBillZip            (String billZip)            { this.orderForm.getOrder().setBillZip            (billZip); }
public void setBillCountry        (String billCountry)         { this.orderForm.getOrder().setBillCountry        (billCountry); }

public String getCardType        ()             { return this.orderForm.getOrder().getCardType         ();}
public String getCreditCard        ()             { return this.orderForm.getOrder().getCreditCard     ();}
public String getExpiryDate        ()             { return this.orderForm.getOrder().getExpiryDate     ();}
public String getBillToFirstName()             { return this.orderForm.getOrder().getBillToFirstName();}
public String getBillToLastName    ()             { return this.orderForm.getOrder().getBillToLastName ();}
public String getBillAddress1    ()             { return this.orderForm.getOrder().getBillAddress1     ();}
public String getBillAddress2    ()             { return this.orderForm.getOrder().getBillAddress2     ();}
public String getBillCity        ()             { return this.orderForm.getOrder().getBillCity         ();}
public String getBillState        ()             { return this.orderForm.getOrder().getBillState         ();}
public String getBillZip        ()             { return this.orderForm.getOrder().getBillZip         ();}
public String getBillCountry    ()             { return this.orderForm.getOrder().getBillCountry     ();}
}

The getOrder() method which returns the OrderData object, is a cool trick to trick the StringFramwork (which will come  looking for an  getOrder() method) and prevent us from creating multiple class files (which is where blind spots tend to occur). I like the fact that I can put all bindable variables in one location/file. There is a small side effect that now we can provide multiple order.* values, ie.  order.order.order.order.order.billCity (I wonder if there is DoS angle here)

And that’s it (this example is a bit simpler since JPetStore doesn’t use a very complex binding wiring (for example no config files to change),  but I will provide more examples later that handle the other variations.

Now, with this fix in place we can verify the fix by running the web automation script that previously allowed us to control the purchase price, and now it doesn’t :)

exploit variation #1

exploit variation #2

November 9, 2011 Posted by | Fixing Code, Java, JPetStore, Spring MVC | Leave a comment

example of spring mvc controllers

here is an example of spring MVC controllers.

Can you spot the vulnerability?

September 15, 2011 Posted by | JPetStore, Spring MVC | 6 Comments

Submit file to Veracode Trial: using Browser and Windows Automation (WatiN and White APIs)

This script automates the process off submitting a file to Veracode’s free trial (as of 20/Jul/2011). See also Consuming Veracode Findings File(s) using O2.

If first provides a settings GUI, where the user can enter the requried data (email and file to upload), then fires up a web brower and uses WatiN (Browser Automation) plus White (Windows UIAutomation) to populate the form fields and to submit the form.

The reason why I had to use UIAutomation (and White API) was because there didn’t seem to be a way to modify the HTML form field of the ‘file upload’ control (even jQuery didn’t seem to be able to modify that value programatially).

The solution was to use UIAutomation to:

  • click on the ‘Browse’ field,
  • then (on the popup file dialog window)
    • enter the file to upload, and
    • click ‘Close’

Here is the ‘settings window:

When the button is clicked the veracode trial page is opened and the ‘Html Form File Upload Button’ is pressed (via UIAutomation) :

When the ‘Choose File to Upload’ window appears, the ‘File Name Text Box’ is populated and the Open Button is pressed (both using UIAutomation (via White API)).

Finally, the email field is populated, the check box is ticked and the ‘Upload’ button is pressed (using Browser Automation (WatiN))

Here is the source code of this script:

var topPanel = O2Gui.open<Panel>("Util - Submit file to Veracode Trial",700,180);    
//var topPanel = panel.clear().add_Panel();
topPanel.insert_LogViewer();
var _email = "o2@o2platform.com";
var _fileToUpload = @"C:\O2\Demos\jPetStore - O2 Demo Pack\apache-tomcat-7.0.16\webapps\jpetstore.war";


Action<string,string> submitFileToVeracode =
	(email, fileToUpload)=>
		{
			var windowName= "Veracode File Upload - {0}".format(10.randomLetters());
			var ie = windowName.popupWindow(1000,500)
							   .add_IE().open("https://trial.veracode.com/freetrials/veracode-free-trial-signup.php");

			var processId = Processes.getCurrentProcessID();
			var apiGuiAutomation = new API_GuiAutomation(processId);
			var window = apiGuiAutomation.window(windowName);
			"got main window".info();
			var buttons = window.buttons();a
			"found {0} buttons".info(buttons.size());
			buttons[1].mouse().click();
			"clicked button".debug();
			var selectFileWindow = apiGuiAutomation.window("Choose File to Upload");
			selectFileWindow.textBoxes()[0].set_Text(fileToUpload);
			selectFileWindow.button("Open").click();
			ie.field("email",email);
			ie.checkBoxes()[0].check();
			ie.button("Upload").click();
			//buttons[2].mouse().click();
		};


topPanel.add_TextBox("Email",_email).top(0)
			.width(100)
			.onTextChange((text)=>_email=text)
		.append_Label("File to upload")
			.autoSize()
			.top(3)
		.append_TextBox(_fileToUpload)
			.onTextChange((text)=>_fileToUpload=text)
			.width(300)
			.align_Right(topPanel);
topPanel.add_Button(24,"Create Account and Upload File")
		.font_bold()
		.align_Right(topPanel)
		.onClick(()=> submitFileToVeracode(_email, _fileToUpload) );

//O2File:WatiN_IE_ExtensionMethods.cs
//using O2.XRules.Database.Utils.O2
//O2Ref:WatiN.Core.1x.dll
//O2File:API_GuiAutomation.cs
//O2Ref:White.Core.dll

July 20, 2011 Posted by | Interoperability, JPetStore, Veracode, WatiN, White_UIAutomation | Leave a comment

Visualizing Spring MVC Annotations based Controls (and Autobinding PetClinic’s vulnerabilities)

If you are reviewing a Spring MVC aplication that uses Java Annotations to define the controller’s behaviour, you can use an older version of O2 (packaged separately) which was published a couple years ago, but is still quite effective in its capabilities.

You can get the installer from here or just execute the following script to download it and start the installation process:

var path = &quot;http://s3.amazonaws.com/O2_Downloads/O2_Cmd_SpringMvc.msi&quot;.uri().download();
path.startProcess();

Once it is installed, you will have a shortcut on your desktop called O2_Cmd_SpringMvc.exe which when executed looks like this:

For the rest of this demo I’m going to use the PetClinc Spring MVC demo app, which you can download from SpringSource or from the O2 Demo Pack that I  published (see Packaged Spring MVC Security Test Apps: JPetStore and PetClinic)

Unzip the files to the C:\O2\Demos folder

And drag and drop the petclinic.classes.zip file into the big red box (top left) from the O2 Spring MVC module, which will trigger the conversion process:

Once the coversion is finished, you will see the Spring MVC mappings and its source code location:

If you select a URL that uses @ModelAttribure, and go to the Spring MVC Bindable fields for selection  tab, you will see a graphical representation of the fields that can be binded into (and that create another variation of the Spring Framework Autobinding vulnerabilities)

Note that in order for the o2 engine to find some of these mappings, there are a couple cases where an extra comment needs to be added to the affected classes.

For example, in the Owner.java class:

    public List&lt;Pet&gt; getPets() { 
    /*O2Helper:MVCAutoBindListObject:org.springframework.samples.petclinic.Pet*/

This O2 tool also has a BlackBox analysis module, which allow the use of the mappings created to drive/test the website dynamically

For example here is how to bypass the current SetDisallowedFields protection that is included in the controller for /editOwner.do

    @InitBinder
    public void setAllowedFields(WebDataBinder dataBinder) {
        dataBinder.setDisallowedFields(new String[] {&quot;id&quot;});
    }

Note the extra field pets[0].owner.id=6 which save the current data into user #6

 

 

This app (together with JPetStore) presents good case-studies on the security vulnerabilities that are easily created with the Spring Autobinding capabilities. In the above example, the Edit Owner page should only allow 5 fields to be edited, while in fact it allows a VERY large number of fields to be edited

In fact, in this case, there are infinite posibilites, since: the Owner class has a getPets method, which returns a Pets class, who has a getOwner method, who has a getPets method , etc….

This applicaition also has a large number of XSS vulnerabilities, including the ability to create a Pet with a XSS payload which (via autobinding) can be assigned to another user (i.e. via modifing the pet’s owner ID)

July 19, 2011 Posted by | JPetStore, Spring MVC | Leave a comment

Util – Java, Jsp and Xml File Search (Example using Spring MVC JPetStore)

Here is a script that creates a regex based file search. The user can define both the file location and search filters.

This is what it looks like when using the files from JPetStore (note: above the files, the textbox on the left is a file filter and the textbox on the right is a regex text search)

This is the source code that creates the above GUI:

var simpleSearch  = O2Gui.open<ascx_SimpleFileSearch>("Util - Java, Jsp and Xml File Search", 900,500);           
simpleSearch.Path.splitContainer().panel1Collapsed(true);  
Action<string, string > loadFiles =
    (path, fileFilters)=>{                            
                            var files = path.files(true,fileFilters.split(",")
                                                                   .ToArray());
                            simpleSearch.loadFiles(path, files); 
                        };
           
var folderToLoad = @"C:\O2\Demos\jPetStore - O2 Demo Pack\sourceCode";
var filter = "*.jsp,*.xml,*.java"; 
Action refresh =
    ()=>{
            loadFiles(folderToLoad,filter);
        };
       
simpleSearch.insert_Above(20)
            .add_TextBox("Path",folderToLoad)
            .onTextChange((text)=> folderToLoad = text)
            .onEnter((text)=> refresh() )  
            .parent()
            .insert_Right(200)
            .add_TextBox("Filter",filter)
            .onTextChange((text)=> filter = text)
            .onEnter((text)=> refresh()) ;
           
refresh();

//O2File:ascx_SimpleFileSearch.cs

July 18, 2011 Posted by | JPetStore, Spring MVC | Leave a comment

Simple Viewer to see JSP files (example using Spring MVC SPetStore)

Here is a simple script that creates a simple viewer for JSP files (note that this version doesn’t support the mapping of internal includes (the next version will))

This tool is available as the Util – View JSPs (simple mode).h2 script and looks like this:

and (note the need to map the include files)

Here is the source code:

//var topPanel = panel.clear().add_Panel();
var topPanel = "Util - View JSPs (simple mode)".popupWindow(1000,300);
var sourceCodeEditor = topPanel.add_SourceCodeEditor();
var treeView = sourceCodeEditor.insert_Left<Panel>(400).add_TreeView().sort();
treeView.afterSelect<string>((file)=>sourceCodeEditor.open(file).setDocumentHighlightingStrategy(".html"));
Action<string,string> loadFilesFromFolder =
    (folder,extension)=>{
                            treeView.clear();
                            foreach(var file in folder.files(extension,true))
                                treeView.add_Node(file.remove(folder),file);
                            treeView.selectFirst();    
                        };</pre>
 

var testFolderWithJsps = @"C:\O2\Demos\jPetStore - O2 Demo Pack\sourceCode\war";                        
topPanel.insert_Above(20)
        .add_TextBox("Path to JSPs",testFolderWithJsps)
        .onEnter((text)=> loadFilesFromFolder(text , "*.jsp"));
                       
loadFilesFromFolder(testFolderWithJsps , "*.jsp");

 

 

July 18, 2011 Posted by | JPetStore, Spring MVC | Leave a comment

Packaged Spring MVC Security Test Apps: JPetStore and PetClinc

If you are looking to learn about Spring MVC Security, one of the best places to start are the test applications that are included with Spring MVC since they contain a number of Security Vulnerabilities that are common in Spring MVC apps (namely the AutoBinding vulnerabilties)

The JPetstore files can be downloaded from:  jPetStore – O2 Demo Pack.zip

The PetClinic files can be downloaded from jPetClinic – O2 Demo Pack.zip

This version of the JPetStore O2 Demo Pack already contains a couple *.H2 files which help with the security testing (PetClinic will soon have them)

July 18, 2011 Posted by | JPetStore, Spring MVC | 2 Comments

Visualizing the links in JPetStore (Spring MVC)

One of the pains of writing web automation scripts for JPetStore is its almost lack of HTML ID tags, which make it very hard to get strong references to the desired (for example) links.

The script below show an IE Automation sequence that will end up in a page where we will grab the links and visualize a possible analysis of its link data:

Here is the script that creates the GUI:

var topPanel = panel.clear().add_Panel();
var ie = topPanel.add_IE().silent(true);

ie.open("http://127.0.0.1.:8080/jpetstore");
ie.link("Enter the Store").click();

var mappings = new Dictionary<string, string>();

foreach(var url in ie.links().urls())
    if(url.contains("categoryId"))   
        mappings.add(url.split("=")[1], url);
       
ie.open(mappings["FISH"]);         
ie.link("FI-FW-01 ").click();

var tableList = topPanel.insert_Left(400).add_TableList();
var urls = from url in ie.links().urls()
              where url.contains("?")         
              select url.replace("?","=");
var results = from url in urls
              select new { address = url.split("=")[0],
                             action =  url.split("=")[1],
                             id = url.split("=")[2] };
             
tableList.show(results);

//ie.inject_jQuery(); 
//ie.inject_FirebugLite();
//return ie.fields();
return "ok";

//O2File:WatiN_IE_ExtensionMethods.cs
//using O2.XRules.Database.Utils.O2
//O2Ref:WatiN.Core.1x.dll

Here is a follow-up script where we create a dictionary that maps the product type to a link:

var topPanel = panel.clear().add_Panel();
var ie = topPanel.add_IE().silent(true);

ie.open("http://127.0.0.1.:8080/jpetstore");
ie.link("Enter the Store").click();

var mappings = new Dictionary<string, string>();

foreach(var url in ie.links().urls())
    if(url.contains("categoryId"))   
        mappings.add(url.split("=")[1], url);
       
ie.open(mappings["FISH"]);         

ie.link("FI-FW-01 ").click();
//O2File:WatiN_IE_ExtensionMethods.cs
//using O2.XRules.Database.Utils.O2
//O2Ref:WatiN.Core.1x.dll

July 15, 2011 Posted by | IE Automation, JPetStore, Spring MVC, WatiN | Leave a comment

Finding the JSP views that are mapped to controlers in JPetStore (Spring MVC)

One of the topics that was discussed at yesterday’s O2 WebCast was ‘how to find the JSPs that are mapped to a controler?’

This is a very important piece of the puzzle since that is one of the few ways we can try to have a fell for the size of the Spring Auto-Binding vulnerabilities (since one of the analysis that needs to be done is the cross-check between what is on the *.jsps and what the Spring MVC engine will autobind into the assigned CommandClass POJO.

The first place to look is on the Spring config file, and here is a script that does exactly that:

var topPanel = panel.clear().add_Panel();
var springConfig = @"C:\O2\Demos\jPetStore - O2 Demo Pack\sourceCode\war\WEB-INF\petstore-servlet.xml";</pre>
&nbsp;

springConfig.showInCodeViewer();
//topPanel.add_SourceCodeViewer().open(springConfig);

var data = from controller in springConfig.springMvcMappings().Controllers
            from property in controller.Properties
            select new { controler = controller.HttpRequestUrl , key = property.Key, value = property.Value };
           
topPanel.add_TableList().show(data);
//return springConfig.springMvcMappings().Controllers[0].Properties[0]; 
//using O2.XRules.Database.Languages_and_Frameworks.J2EE
//using O2.XRules.Database.APIs.IKVM
//O2File:spring-servlet-2.0.xsd.cs
//O2File:SpringMvcMappings_v2.0.cs
//O2Ref:O2_Misc_Microsoft_MPL_Libs.dll

When executed this script will open up the local petstore-servlet.xml config file and show a mapping the bean URLs and its property values

As the table created clearly shows, in this application (and this tend to be unique per app) we can’t use the spring config file to fully map the Controller’s views (there are a couple that can be resolved using this method, but there are a lot of missing mappings).

A quick look at one of the controlllers shows the most likely solution

Which is the fact that the view seems to be set by the contructor of the ModeAndView object:

        return new ModelAndView("ViewOrder", model);

The question now is: “How often was this pattern used in JPetStore?”.

A first look looks that is common


Which is confirmed when looking at the other ModelAndView constructors:

So to correctly map these Jsps we are going to have to programatically retrieve the first string value of the ModelAndView constructor and map it to the correct controller.

This can be done using the metadata that is exposed from the O2 Java Static Analysis Engine (in the screenshot below note the string value two instructions before the call to the ModelAndView constructor):

Once we have the Jsps we will need to find the fields that are used inside that Jsps.

At the moment O2 doesn’t have a JSP file parser, but this information should be also retrivable from the created *.class files, which in this case can be found in Tomcat’s temp folder  (C:\O2\Demos\jPetStore – O2 Demo Pack\apache-tomcat-7.0.16\work\Catalina\localhost\jpetstore\org\apache\jsp\WEB_002dINF\jsp\spring):

July 15, 2011 Posted by | Java, JPetStore, Spring MVC | Leave a comment

Viewing JPetStore Hsqldb database and couple more Autobinding issues

Included in the hsqldb folder is a file called manager.bat which can be used to connect to the live (in memory) JPetStore database.

Here are the settings needed (select ‘HSQL Database Engine Server’ and use jdbc:hsqldb:hsql://localhost:9002 as the URL value):

Here is a sample query:

 
Here are a couple more Autobinding issues
var ie = topPanel.add_IE_with_NavigationBar().silent(true); 
 
 Action scrollToTotal = 
    ()=>{
            var tdElement = ie.elements().elements("TD").toList().Where((element)=> element.innerHtml().notNull() && element.innerHtml().contains("Total:")).first();
            tdElement.scrollIntoView();                                                                                                
        };
       
//var _payload = "order.totalPrice=1111&" + "order.lineItems[0].unitPrice=12";  // works
//var _payload = "order.totalPrice=1111&" + "order.lineItems[0].itemId=AA";  // throws error
//var _payload = "order.totalPrice=1111&" + "order.lineItems[0].itemId=EST-6";  // takes the inventory from EST-6 and not EST-4
var _payload = "order.totalPrice=1111&" + "order.lineItems[0].item.product.name=AAAABBBB"; // changes the visible product name</pre>
&nbsp;

var jPetStore = new API_JPetStore(ie);
jPetStore.homePage(); 
jPetStore.loginPlaceAnOrderAndGoToCheckout();
ie.buttons()[1].flash().click(); 
jPetStore.open("/shop/newOrder.do?_finish=true&" + _payload);
scrollToTotal();                   

July 13, 2011 Posted by | JPetStore, Spring MVC | Leave a comment

Follow

Get every new post delivered to your Inbox.